2019-11-12 23:51:27 +00:00
import os
import shutil
import subprocess
2020-02-19 17:56:11 +01:00
import sys
2019-11-12 23:51:27 +00:00
import tempfile
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
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-05-09 10:15:26 +01:00
from .typing import PathOrStr
2021-01-06 13:50:58 -05:00
from .util import (
BuildOptions ,
BuildSelector ,
NonPlatformWheelError ,
download ,
get_build_verbosity_extra_flags ,
prepare_command ,
2021-01-09 15:40:40 -05:00
read_python_configs ,
2021-01-06 13:50:58 -05:00
)
2017-04-10 21:42:12 +01:00
2019-10-26 01:09:19 +02:00
2021-04-30 17:56:34 -04:00
def call (
2021-05-09 10:15:26 +01:00
args : Sequence [ PathOrStr ], env : Optional [ Dict [ str , str ]] = None , cwd : Optional [ str ] = None
) -> None :
2021-05-03 11:45:43 -04:00
print ( "+ " + " " . join ( str ( a ) for a in args ))
2020-07-08 18:17:55 +01:00
# we use shell=True here, even though we don't need a shell due to a bug
# https://bugs.python.org/issue8557
2021-05-09 10:15:26 +01:00
subprocess . run ([ str ( a ) for a in args ], env = env , cwd = cwd , shell = True , check = True )
2020-07-08 18:17:55 +01:00
def shell ( command : str , env : Optional [ Dict [ str , str ]] = None , cwd : Optional [ str ] = None ) -> None :
2021-05-03 11:45:43 -04:00
print ( f "+ { command } " )
2021-02-14 12:56:33 -05:00
subprocess . run ( command , env = env , cwd = cwd , shell = True , check = True )
2020-02-11 14:44:53 +01:00
2020-04-08 00:16:25 +02:00
def get_nuget_args ( version : str , arch : str ) -> List [ str ]:
2021-02-14 20:44:20 +01:00
python_name = "python"
2021-05-03 11:45:43 -04:00
if arch == "32" :
python_name += "x86"
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" ,
"C: \\ cibw \\ python" ,
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-01-01 16:53:45 -05:00
map_arch = {
2021-05-03 11:45:43 -04:00
"32" : Architecture . x86 ,
"64" : Architecture . AMD64 ,
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 :
2020-02-11 14:44:53 +01:00
with ZipFile ( zip_src ) as zip :
zip . extractall ( dest )
2020-06-15 01:53:31 +02:00
def install_cpython ( version : str , arch : str , nuget : Path ) -> Path :
2020-02-11 14:44:53 +01:00
nuget_args = get_nuget_args ( version , arch )
2021-05-03 11:45:43 -04:00
installation_path = Path ( nuget_args [ - 1 ]) / ( nuget_args [ 0 ] + "." + version ) / "tools"
call ([ nuget , "install" , * nuget_args ])
2020-02-11 14:44:53 +01:00
return installation_path
2020-06-15 01:53:31 +02:00
def install_pypy ( version : str , arch : str , url : str ) -> Path :
2021-05-03 11:45:43 -04:00
assert arch == "32"
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 )
2021-05-03 11:45:43 -04:00
installation_path = Path ( "C: \\ cibw" ) / zip_filename [: - len ( extension )]
2020-06-15 01:53:31 +02:00
if not installation_path . exists ():
2021-05-03 11:45:43 -04:00
pypy_zip = Path ( "C: \\ cibw" ) / zip_filename
2020-02-11 14:44:53 +01:00
download ( url , pypy_zip )
# Extract to the parent directory because the zip file still contains a directory
2020-06-15 01:53:31 +02:00
extract_zip ( pypy_zip , installation_path . parent )
2021-02-14 20:44:20 +01:00
( installation_path / "python.exe" ) . symlink_to ( installation_path / "pypy3.exe" )
2020-02-11 14:44:53 +01:00
return installation_path
2021-04-30 17:56:34 -04:00
def setup_python (
python_configuration : PythonConfiguration ,
dependency_constraint_flags : Sequence [ PathOrStr ],
environment : ParsedEnvironment ,
) -> Dict [ str , str ]:
2021-05-03 11:45:43 -04:00
nuget = Path ( "C: \\ cibw \\ nuget.exe" )
2020-06-15 01:53:31 +02:00
if not nuget . exists ():
2021-05-03 11:45:43 -04:00
log . step ( "Downloading nuget..." )
download ( "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe" , nuget )
2020-04-05 16:03:35 +02:00
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 } ..." )
2020-11-13 16:30:27 +00:00
2021-05-03 11:45:43 -04:00
if implementation_id . startswith ( "cp" ):
2021-04-30 17:56:34 -04:00
installation_path = install_cpython (
python_configuration . version , python_configuration . arch , nuget
)
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
2021-04-30 17:56:34 -04:00
installation_path = install_pypy (
python_configuration . version , python_configuration . arch , python_configuration . url
)
2020-04-05 16:03:35 +02:00
else :
raise ValueError ( "Unknown Python implementation" )
2021-05-03 11:45:43 -04:00
assert ( installation_path / "python.exe" ) . exists ()
2020-04-05 16:03:35 +02:00
2021-05-03 11:45:43 -04:00
log . step ( "Setting up build environment..." )
2020-11-13 16:30:27 +00:00
2020-04-05 16:03:35 +02:00
# set up PATH and environment variables for run_with_env
env = os . environ . copy ()
2021-05-03 11:45:43 -04:00
env [ "PYTHON_VERSION" ] = python_configuration . version
env [ "PYTHON_ARCH" ] = python_configuration . arch
env [ "PATH" ] = os . pathsep . join (
[ str ( installation_path ), str ( installation_path / "Scripts" ), env [ "PATH" ]]
2021-04-30 17:56:34 -04:00
)
2021-05-03 11:45:43 -04:00
env [ "PIP_DISABLE_PIP_VERSION_CHECK" ] = "1"
2021-01-24 15:08:46 +00:00
2020-04-05 16:03:35 +02:00
# update env with results from CIBW_ENVIRONMENT
env = environment . as_dictionary ( prev_environment = env )
# for the logs - check we're running the right version of python
2021-05-03 11:45:43 -04:00
call ([ "where" , "python" ], env = env )
call ([ "python" , "--version" ], env = env )
call ([ "python" , "-c" , " \" import struct; print(struct.calcsize('P') * 8) \" " ], env = env )
2021-04-30 17:56:34 -04:00
where_python = (
subprocess . run (
2021-05-03 11:45:43 -04:00
[ "where" , "python" ],
2021-04-30 17:56:34 -04:00
env = env ,
universal_newlines = True ,
check = True ,
stdout = subprocess . PIPE ,
)
. stdout . splitlines ()[ 0 ]
. strip ()
)
2021-05-03 11:45:43 -04:00
if where_python != str ( installation_path / "python.exe" ):
2021-04-30 17:56:34 -04: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 ,
)
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-04-05 16:03:35 +02:00
2021-05-08 18:03:35 +01:00
log . step ( "Installing build tools..." )
2021-05-13 20:32:38 +02:00
# ensure pip is installed
2021-05-16 10:12:08 +01:00
call ([ "python" , "-m" , "ensurepip" ], env = env , cwd = "C: \\ cibw" )
2021-05-15 17:04:39 +01:00
2021-05-16 10:12:08 +01:00
# upgrade to the version matching our constraints
# if necessary, reinstall it to ensure that it's installed as 'pip.exe'
requires_reinstall = not ( installation_path / "Scripts" / "pip.exe" ) . exists ()
2021-05-13 20:32:38 +02:00
call (
2021-05-16 10:12:08 +01:00
[
"python" ,
"-m" ,
"pip" ,
"install" ,
"--force-reinstall" if requires_reinstall else "--upgrade" ,
"pip" ,
* dependency_constraint_flags ,
],
2021-05-13 20:32:38 +02:00
env = env ,
)
2021-05-08 18:03:35 +01:00
2021-05-03 11:45:43 -04:00
assert ( installation_path / "Scripts" / "pip.exe" ) . exists ()
2021-04-30 17:56:34 -04:00
where_pip = (
subprocess . run (
2021-05-03 11:45:43 -04:00
[ "where" , "pip" ], env = env , universal_newlines = True , check = True , stdout = subprocess . PIPE
2021-04-30 17:56:34 -04:00
)
. stdout . splitlines ()[ 0 ]
. strip ()
)
2021-05-03 11:45:43 -04:00
if where_pip . strip () != str ( installation_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
2021-05-03 11:45:43 -04:00
call ([ "pip" , "--version" ], env = env )
2021-04-30 17:56:34 -04:00
call (
2021-05-03 11:45:43 -04:00
[ "pip" , "install" , "--upgrade" , "setuptools" , "wheel" , * dependency_constraint_flags ],
2021-04-30 17:56:34 -04:00
env = env ,
)
2020-04-05 16:03:35 +02:00
2020-06-14 11:25:14 -04:00
return env
2020-04-10 01:44:02 +02:00
def build ( options : BuildOptions ) -> None :
2021-05-03 11:45:43 -04:00
temp_dir = Path ( tempfile . mkdtemp ( prefix = "cibuildwheel" ))
built_wheel_dir = temp_dir / "built_wheel"
repaired_wheel_dir = temp_dir / "repaired_wheel"
2017-07-02 18:02:27 -05:00
2020-11-13 16:30:27 +00:00
try :
if options . before_all :
2021-05-03 11:45:43 -04:00
log . step ( "Running before_all..." )
2020-11-13 16:30:27 +00:00
env = options . environment . as_dictionary ( prev_environment = os . environ )
2021-04-30 17:56:34 -04:00
before_all_prepared = prepare_command (
2021-05-03 11:45:43 -04:00
options . before_all , project = "." , package = options . package_dir
2021-04-30 17:56:34 -04:00
)
2020-11-13 16:30:27 +00:00
shell ( before_all_prepared , env = env )
2019-04-27 10:20:37 +01:00
2021-04-30 17:56:34 -04:00
python_configurations = get_python_configurations (
options . build_selector , options . architectures
)
2020-05-11 17:09:54 +02:00
2020-11-13 16:30:27 +00:00
for config in python_configurations :
log . build_start ( config . identifier )
2020-04-06 16:55:15 +02:00
2021-01-02 14:32:55 -05:00
dependency_constraint_flags : Sequence [ PathOrStr ] = []
2020-11-13 16:30:27 +00:00
if options . dependency_constraints :
dependency_constraint_flags = [
2021-05-03 11:45:43 -04:00
"-c" ,
2021-04-30 17:56:34 -04:00
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
env = setup_python ( config , dependency_constraint_flags , options . environment )
2017-04-10 21:42:12 +01:00
2020-11-13 16:30:27 +00:00
# run the before_build command
if 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-05-03 11:45:43 -04:00
options . before_build , project = "." , package = options . 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..." )
2020-11-13 16:30:27 +00:00
if built_wheel_dir . exists ():
shutil . rmtree ( built_wheel_dir )
built_wheel_dir . mkdir ( parents = True )
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369
2021-04-30 17:56:34 -04:00
call (
[
2021-05-03 11:45:43 -04:00
"pip" ,
"wheel" ,
2021-04-30 17:56:34 -04:00
options . package_dir . resolve (),
2021-05-03 11:45:43 -04:00
"-w" ,
2021-04-30 17:56:34 -04:00
built_wheel_dir ,
2021-05-03 11:45:43 -04:00
"--no-deps" ,
2021-04-30 17:56:34 -04:00
* get_build_verbosity_extra_flags ( options . build_verbosity ),
],
env = env ,
)
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
if repaired_wheel_dir . exists ():
shutil . rmtree ( repaired_wheel_dir )
repaired_wheel_dir . mkdir ( parents = True )
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
2020-11-13 16:30:27 +00:00
if 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 (
options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir
)
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-01-20 21:22:48 -05:00
if options . test_command and options . 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.
2021-05-03 11:45:43 -04:00
call ([ "pip" , "install" , "virtualenv" , * dependency_constraint_flags ], env = env )
2020-11-13 16:30:27 +00:00
venv_dir = Path ( tempfile . mkdtemp ())
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
2021-05-03 11:45:43 -04: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
2021-05-03 11:45:43 -04:00
call ([ "where" , "python" ], env = virtualenv_env )
2020-11-13 16:30:27 +00:00
if options . before_test :
before_test_prepared = prepare_command (
options . before_test ,
2021-05-03 11:45:43 -04:00
project = "." ,
2021-04-29 20:21:42 -04:00
package = 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 (
2021-05-03 11:45:43 -04:00
[ "pip" , "install" , str ( repaired_wheel ) + 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
if options . test_requires :
2021-05-03 11:45:43 -04:00
call ([ "pip" , "install" ] + 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 (
options . test_command ,
2021-05-03 11:45:43 -04:00
project = Path ( "." ) . resolve (),
2021-04-29 20:21:42 -04:00
package = options . 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
# clean up
shutil . rmtree ( venv_dir )
2019-10-12 10:47:00 +01:00
2020-11-13 16:30:27 +00:00
# we're all done here; move it to output (remove if already exists)
shutil . move ( str ( repaired_wheel ), options . output_dir )
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 )