2019-11-12 23:51:27 +00:00
import os
import shutil
import subprocess
2020-02-19 17:56:11 +01:00
import sys
2022-01-08 11:30:48 +01:00
from functools import lru_cache
2020-06-15 01:53:31 +02:00
from pathlib import Path
2021-05-08 19:24:40 +01:00
from typing import Dict , List , NamedTuple , Optional , Sequence , Set
2019-11-07 00:55:58 +01:00
from zipfile import ZipFile
2020-11-13 16:30:27 +00:00
2022-01-08 11:30:48 +01:00
from filelock import FileLock
2021-11-24 10:50:49 -08:00
from packaging.version import Version
2021-01-22 09:33:22 -05: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
2021-06-23 10:47:18 -04:00
from .typing import PathOrStr , assert_never
2021-01-06 13:50:58 -05:00
from .util import (
2022-01-08 11:30:48 +01:00
CIBW_CACHE_PATH ,
2021-06-23 10:47:18 -04:00
BuildFrontend ,
2021-01-06 13:50:58 -05:00
BuildSelector ,
NonPlatformWheelError ,
2022-01-05 02:59:24 +01:00
call ,
2021-01-06 13:50:58 -05:00
download ,
get_build_verbosity_extra_flags ,
2021-06-23 10:47:18 -04:00
get_pip_version ,
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-01-08 11:30:48 +01:00
virtualenv ,
2021-01-06 13:50:58 -05:00
)
2017-04-10 21:42:12 +01:00
2021-07-09 09:16:20 -04:00
2022-01-08 11:30:48 +01:00
def get_nuget_args ( version : str , arch : str , output_directory : Path ) -> List [ str ]:
2021-11-24 10:50:49 -08:00
platform_suffix = { "32" : "x86" , "64" : "" , "ARM64" : "arm64" }
python_name = "python" + platform_suffix [ arch ]
2021-04-30 22:41:19 +01:00
return [
python_name ,
2021-05-03 11:45:43 -04:00
"-Version" ,
2021-04-30 17:56:34 -04:00
version ,
2021-05-03 11:45:43 -04:00
"-FallbackSource" ,
"https://api.nuget.org/v3/index.json" ,
"-OutputDirectory" ,
2022-01-08 11:30:48 +01:00
str ( output_directory ),
2021-04-30 22:41:19 +01:00
]
2019-10-23 22:53:10 +02:00
2019-11-12 23:51:27 +00:00
2020-04-10 01:53:06 +02:00
class PythonConfiguration ( NamedTuple ):
version : str
arch : str
identifier : str
2021-01-09 15:40:40 -05:00
url : Optional [ str ] = None
2020-04-08 00:16:25 +02:00
2021-01-09 15:40:40 -05:00
def get_python_configurations (
2021-04-30 17:56:34 -04:00
build_selector : BuildSelector ,
architectures : Set [ Architecture ],
2021-01-09 15:40:40 -05:00
) -> List [ PythonConfiguration ]:
2021-05-03 11:45:43 -04:00
full_python_configs = read_python_configs ( "windows" )
2021-01-09 15:40:40 -05:00
python_configurations = [ PythonConfiguration ( ** item ) for item in full_python_configs ]
2021-11-24 10:50:49 -08:00
map_arch = { "32" : Architecture . x86 , "64" : Architecture . AMD64 , "ARM64" : Architecture . ARM64 }
2021-01-01 16:53:45 -05:00
2019-11-12 23:51:27 +00:00
# skip builds as required
2021-01-01 16:53:45 -05:00
python_configurations = [
2021-04-30 17:56:34 -04:00
c
for c in python_configurations
2021-01-01 16:53:45 -05:00
if build_selector ( c . identifier ) and map_arch [ c . arch ] in architectures
]
2019-09-28 19:24:16 +02:00
2019-10-23 22:53:10 +02:00
return python_configurations
2019-04-27 10:20:37 +01:00
2019-04-20 18:52:01 +02:00
2020-06-15 01:53:31 +02:00
def extract_zip ( zip_src : Path , dest : Path ) -> None :
2021-12-27 09:26:33 +01:00
with ZipFile ( zip_src ) as zip_ :
zip_ . extractall ( dest )
2020-02-11 14:44:53 +01:00
2022-01-08 11:30:48 +01:00
@lru_cache ( maxsize = None )
def _ensure_nuget () -> Path :
nuget = CIBW_CACHE_PATH / "nuget.exe"
with FileLock ( str ( nuget ) + ".lock" ):
if not nuget . exists ():
download ( "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" , nuget )
return nuget
2020-02-11 14:44:53 +01:00
2022-01-08 11:30:48 +01:00
def install_cpython ( version : str , arch : str ) -> Path :
base_output_dir = CIBW_CACHE_PATH / "nuget-cpython"
nuget_args = get_nuget_args ( version , arch , base_output_dir )
installation_path = base_output_dir / ( nuget_args [ 0 ] + "." + version ) / "tools"
with FileLock ( str ( base_output_dir ) + f "- { version } - { arch } .lock" ):
if not installation_path . exists ():
nuget = _ensure_nuget ()
call ( nuget , "install" , * nuget_args )
return installation_path / "python.exe"
def install_pypy ( tmp : Path , arch : str , url : str ) -> Path :
2021-05-12 14:42:09 +00:00
assert arch == "64" and "win64" in url
2020-02-11 14:44:53 +01:00
# Inside the PyPy zip file is a directory with the same name
2021-05-03 11:45:43 -04:00
zip_filename = url . rsplit ( "/" , 1 )[ - 1 ]
2020-06-15 01:53:31 +02:00
extension = ".zip"
assert zip_filename . endswith ( extension )
2022-01-08 11:30:48 +01:00
installation_path = CIBW_CACHE_PATH / zip_filename [: - len ( extension )]
with FileLock ( str ( installation_path ) + ".lock" ):
if not installation_path . exists ():
pypy_zip = tmp / zip_filename
download ( url , pypy_zip )
# Extract to the parent directory because the zip file still contains a directory
extract_zip ( pypy_zip , installation_path . parent )
return installation_path / "python.exe"
2020-02-11 14:44:53 +01:00
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 ,
2021-06-23 10:47:18 -04:00
build_frontend : BuildFrontend ,
2021-04-30 17:56:34 -04:00
) -> Dict [ str , str ]:
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" ):
2022-01-08 11:30:48 +01:00
base_python = install_cpython ( python_configuration . version , python_configuration . arch )
2021-05-03 11:45:43 -04:00
elif implementation_id . startswith ( "pp" ):
2020-04-10 20:24:52 +02:00
assert python_configuration . url is not None
2022-01-08 11:30:48 +01:00
base_python = install_pypy ( tmp , python_configuration . arch , python_configuration . url )
2020-04-05 16:03:35 +02:00
else :
raise ValueError ( "Unknown Python implementation" )
2022-01-08 11:30:48 +01:00
assert base_python . exists ()
2020-04-05 16:03:35 +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"
env = virtualenv ( base_python , venv_path , dependency_constraint_flags )
2020-11-13 16:30:27 +00:00
2022-01-08 11:30:48 +01:00
# set up environment variables for run_with_env
2021-05-03 11:45:43 -04:00
env [ "PYTHON_VERSION" ] = python_configuration . version
env [ "PYTHON_ARCH" ] = python_configuration . arch
env [ "PIP_DISABLE_PIP_VERSION_CHECK" ] = "1"
2021-01-24 15:08:46 +00:00
2021-11-24 10:50:49 -08:00
# pip older than 21.3 builds executables such as pip.exe for x64 platform.
# The first re-install of pip updates pip module but builds pip.exe using
# the old pip which still generates x64 executable. But the second
# re-install uses updated pip and correctly builds pip.exe for the target.
# This can be removed once ARM64 Pythons (currently 3.9 and 3.10) bundle
# pip versions newer than 21.3.
if python_configuration . arch == "ARM64" and Version ( get_pip_version ( env )) < Version ( "21.3" ):
call (
2022-01-05 02:59:24 +01:00
"python" ,
"-m" ,
"pip" ,
"install" ,
"--force-reinstall" ,
"--upgrade" ,
"pip" ,
* dependency_constraint_flags ,
2021-11-24 10:50:49 -08:00
env = env ,
2022-01-08 11:30:48 +01:00
cwd = venv_path ,
2021-11-24 10:50:49 -08: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.exe'
2021-05-13 20:32:38 +02:00
call (
2022-01-05 02:59:24 +01:00
"python" ,
"-m" ,
"pip" ,
"install" ,
2022-01-08 11:30:48 +01:00
"--upgrade" ,
2022-01-05 02:59:24 +01:00
"pip" ,
* dependency_constraint_flags ,
2021-05-13 20:32:38 +02:00
env = env ,
2022-01-08 11:30:48 +01:00
cwd = venv_path ,
2021-05-13 20:32:38 +02:00
)
2021-05-08 18:03:35 +01:00
2021-12-13 09:39:41 -05:00
# update env with results from CIBW_ENVIRONMENT
env = environment . as_dictionary ( prev_environment = env )
# check what Python version we're on
2022-01-05 02:59:24 +01:00
call ( "where" , "python" , env = env )
call ( "python" , "--version" , env = env )
call ( "python" , "-c" , " \" import struct; print(struct.calcsize('P') * 8) \" " , env = env )
where_python = call ( "where" , "python" , env = env , capture_stdout = True ) . splitlines ()[ 0 ] . strip ()
2022-01-08 11:30:48 +01:00
if where_python != str ( venv_path / "Scripts" / "python.exe" ):
2021-12-13 09:39:41 -05:00
print (
"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." ,
file = sys . stderr ,
)
sys . exit ( 1 )
# check what pip version we're on
2022-01-08 11:30:48 +01:00
assert ( venv_path / "Scripts" / "pip.exe" ) . exists ()
2022-01-05 02:59:24 +01:00
where_pip = call ( "where" , "pip" , env = env , capture_stdout = True ) . splitlines ()[ 0 ] . strip ()
2022-01-08 11:30:48 +01:00
if where_pip . strip () != str ( venv_path / "Scripts" / "pip.exe" ):
2021-04-30 17:56:34 -04:00
print (
"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." ,
file = sys . stderr ,
)
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-04-05 16:03:35 +02:00
2022-01-05 02:59:24 +01:00
call ( "pip" , "--version" , env = env )
2021-06-23 10:47:18 -04:00
2022-01-08 11:30:48 +01: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" ,
"setuptools" ,
"wheel" ,
* 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" ,
"build[virtualenv]" ,
* dependency_constraint_flags ,
2021-06-23 10:47:18 -04:00
env = env ,
)
else :
assert_never ( build_frontend )
2020-04-05 16:03:35 +02:00
2020-06-14 11:25:14 -04:00
return env
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-04-19 16:00:12 +01:00
if len ( python_configurations ) == 0 :
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 )
before_all_prepared = prepare_command (
before_all_options . before_all , project = "." , package = options . globals . package_dir
)
shell ( before_all_prepared , env = env )
2019-04-27 10:20:37 +01: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 )
2020-11-13 16:30:27 +00:00
log . build_start ( config . identifier )
2020-04-06 16:55:15 +02: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-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
]
2017-03-19 21:27:29 +00:00
2020-11-13 16:30:27 +00:00
# install Python
2021-06-23 10:47:18 -04:00
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 ,
build_options . build_frontend ,
2021-06-23 10:47:18 -04:00
)
2017-04-10 21:42:12 +01:00
2020-11-13 16:30:27 +00:00
# run the before_build command
2021-10-12 02:05:47 +01:00
if build_options . before_build :
2021-05-03 11:45:43 -04:00
log . step ( "Running before_build..." )
2021-04-30 17:56:34 -04:00
before_build_prepared = prepare_command (
2021-10-12 02:05:47 +01:00
build_options . before_build , project = "." , package = options . globals . package_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell ( before_build_prepared , env = env )
2020-06-14 11:25:14 -04:00
2021-05-03 11:45:43 -04:00
log . step ( "Building wheel..." )
2022-01-08 11:30:48 +01:00
built_wheel_dir . mkdir ()
2021-06-23 10:47:18 -04:00
2021-10-12 02:05:47 +01:00
verbosity_flags = get_build_verbosity_extra_flags ( build_options . build_verbosity )
2021-06-23 10:47:18 -04:00
2021-10-12 02:05:47 +01:00
if build_options . build_frontend == "pip" :
2021-06-23 10:47:18 -04: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 (
2022-01-05 02:59:24 +01:00
"python" ,
"-m" ,
"pip" ,
"wheel" ,
options . globals . package_dir . resolve (),
f "--wheel-dir= { built_wheel_dir } " ,
"--no-deps" ,
* get_build_verbosity_extra_flags ( build_options . build_verbosity ),
2021-06-23 10:47:18 -04:00
env = env ,
)
2021-10-12 02:05:47 +01:00
elif build_options . build_frontend == "build" :
2021-06-23 10:47:18 -04:00
config_setting = " " . join ( verbosity_flags )
build_env = env . copy ()
2021-10-12 02:05:47 +01:00
if build_options . dependency_constraints :
constraints_path = build_options . dependency_constraints . get_for_python_version (
2021-07-09 09:16:20 -04:00
config . version
)
# Bug in pip <= 21.1.3 - we can't have a space in the
# constraints file, and pip doesn't support drive letters
# in uhi. After probably pip 21.2, we can use uri. For
# now, use a temporary file.
if " " in str ( constraints_path ):
2022-01-08 11:30:48 +01:00
assert " " not in str ( identifier_tmp_dir )
tmp_file = identifier_tmp_dir / "constraints.txt"
tmp_file . write_bytes ( constraints_path . read_bytes ())
constraints_path = tmp_file
2021-07-09 09:16:20 -04:00
build_env [ "PIP_CONSTRAINT" ] = str ( constraints_path )
build_env [ "VIRTUALENV_PIP" ] = get_pip_version ( env )
call (
2022-01-05 02:59:24 +01:00
"python" ,
"-m" ,
"build" ,
build_options . package_dir ,
"--wheel" ,
f "--outdir= { built_wheel_dir } " ,
f "--config-setting= { config_setting } " ,
2021-07-09 09:16:20 -04:00
env = build_env ,
2021-06-23 10:47:18 -04:00
)
else :
2021-10-12 02:05:47 +01:00
assert_never ( build_options . build_frontend )
2019-11-12 23:34:59 +00:00
2021-05-03 11:45:43 -04:00
built_wheel = next ( built_wheel_dir . glob ( "*.whl" ))
2020-07-20 15:35:51 +01:00
2020-11-13 16:30:27 +00:00
# repair the wheel
2022-01-08 11:30:48 +01:00
repaired_wheel_dir . mkdir ()
2020-07-20 15:35:51 +01:00
2021-05-03 11:45:43 -04:00
if built_wheel . name . endswith ( "none-any.whl" ):
2020-11-13 16:30:27 +00:00
raise NonPlatformWheelError ()
2020-07-20 15:35:51 +01:00
2021-10-12 02:05:47 +01:00
if build_options . repair_command :
2021-05-03 11:45:43 -04:00
log . step ( "Repairing wheel..." )
2021-04-30 17:56:34 -04:00
repair_command_prepared = prepare_command (
2021-10-12 02:05:47 +01:00
build_options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell ( repair_command_prepared , env = env )
else :
shutil . move ( str ( built_wheel ), repaired_wheel_dir )
2017-03-19 21:27:29 +00:00
2021-05-03 11:45:43 -04:00
repaired_wheel = next ( repaired_wheel_dir . glob ( "*.whl" ))
2020-03-01 11:09:22 +00:00
2021-10-12 02:05:47 +01:00
if build_options . test_command and options . globals . test_selector ( config . identifier ):
2021-05-03 11:45:43 -04:00
log . step ( "Testing wheel..." )
2020-11-13 16:30:27 +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.
2022-01-05 02:59:24 +01:00
call ( "pip" , "install" , "virtualenv" , * dependency_constraint_flags , env = env )
2022-01-08 11:30:48 +01:00
venv_dir = identifier_tmp_dir / "venv-test"
2019-10-12 10:47:00 +01:00
2020-11-13 16:30:27 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
2022-01-05 02:59:24 +01:00
call ( "python" , "-m" , "virtualenv" , "--no-download" , venv_dir , env = env )
2019-10-12 10:47:00 +01:00
2020-11-13 16:30:27 +00:00
virtualenv_env = env . copy ()
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 / "Scripts" ),
virtualenv_env [ "PATH" ],
2021-04-30 17:56:34 -04:00
]
)
2019-10-12 10:47:00 +01:00
2020-11-13 16:30:27 +00:00
# check that we are using the Python from the virtual environment
2022-01-05 02:59:24 +01:00
call ( "where" , "python" , env = virtualenv_env )
2020-11-13 16:30:27 +00:00
2021-10-12 02:05:47 +01:00
if build_options . before_test :
2020-11-13 16:30:27 +00:00
before_test_prepared = prepare_command (
2021-10-12 02:05:47 +01:00
build_options . before_test ,
2021-05-03 11:45:43 -04:00
project = "." ,
2021-10-12 02:05:47 +01:00
package = build_options . package_dir ,
2020-11-13 16:30:27 +00:00
)
shell ( before_test_prepared , env = virtualenv_env )
# install the wheel
2021-04-30 17:56:34 -04:00
call (
2022-01-05 02:59:24 +01:00
"pip" ,
"install" ,
str ( repaired_wheel ) + build_options . test_extras ,
2021-04-30 17:56:34 -04:00
env = virtualenv_env ,
)
2020-11-13 16:30:27 +00:00
# test the wheel
2021-10-12 02:05:47 +01:00
if build_options . test_requires :
2022-01-05 02:59:24 +01:00
call ( "pip" , "install" , * build_options . test_requires , env = virtualenv_env )
2020-11-13 16:30:27 +00:00
# run the tests from c:\, 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 ,
2021-05-03 11:45:43 -04:00
project = Path ( "." ) . resolve (),
2021-10-12 02:05:47 +01:00
package = options . globals . package_dir . resolve (),
2020-04-09 16:19:36 +01:00
)
2021-05-03 11:45:43 -04:00
shell ( test_command_prepared , cwd = "c: \\ " , env = virtualenv_env )
2020-01-07 00:17:03 +01:00
2020-11-13 16:30:27 +00:00
# we're all done here; move it to output (remove if already exists)
2021-10-12 02:05:47 +01:00
shutil . move ( str ( repaired_wheel ), build_options . output_dir )
2022-01-08 11:30:48 +01:00
# clean up
# (we ignore errors because occasionally Windows fails to unlink a file and we
# don't want to abort a build because of that)
shutil . rmtree ( identifier_tmp_dir , ignore_errors = True )
2020-11-13 16:30:27 +00:00
log . build_end ()
except subprocess . CalledProcessError as error :
2021-04-30 17:56:34 -04:00
log . step_end_with_error (
2021-05-03 11:45:43 -04:00
f "Command { error . cmd } failed with code { error . returncode } . { error . stdout } "
2021-04-30 17:56:34 -04:00
)
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )