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
2017-03-19 21:27:29 +00:00
from collections import namedtuple
2017-07-02 18:02:27 -05:00
from glob import glob
2019-11-07 00:55:58 +01:00
from zipfile import ZipFile
2017-03-19 21:27:29 +00:00
2019-11-12 23:51:27 +00:00
from .util import (
download ,
get_build_verbosity_extra_flags ,
prepare_command ,
2020-02-15 11:45:54 +00:00
get_pip_script ,
2020-04-06 21:33:13 +02:00
BuildOptions
2019-11-12 23:51:27 +00:00
)
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2019-03-10 22:41:55 +00:00
IS_RUNNING_ON_AZURE = os . path . exists ( 'C: \\ hostedtoolcache' )
2019-10-23 22:53:10 +02:00
IS_RUNNING_ON_TRAVIS = os . environ . get ( 'TRAVIS_OS_NAME' ) == 'windows'
2019-02-22 21:03:04 +00:00
2019-10-26 01:09:19 +02:00
2020-03-06 10:14:54 +00:00
def shell ( args , env = None , cwd = None ):
2020-02-11 14:44:53 +01:00
print ( '+ ' + ' ' . join ( args ))
2020-03-07 12:17:49 +00:00
return subprocess . check_call ( ' ' . join ( args ), env = env , cwd = cwd , shell = True )
2020-02-11 14:44:53 +01:00
2019-12-26 20:30:58 +01:00
def get_nuget_args ( version , arch ):
python_name = 'python' if version [ 0 ] == '3' else 'python2'
if arch == '32' :
python_name = python_name + 'x86'
return [ python_name , '-Version' , version , '-OutputDirectory' , 'C: \\ cibw \\ python' ]
2019-10-23 22:53:10 +02:00
2019-11-12 23:51:27 +00:00
2019-04-20 18:52:01 +02:00
def get_python_configurations ( build_selector ):
2019-11-07 00:55:58 +01:00
PythonConfiguration = namedtuple ( 'PythonConfiguration' , [ 'version' , 'arch' , 'identifier' , 'url' ])
2019-04-20 18:52:01 +02:00
python_configurations = [
2020-03-18 10:11:09 +00:00
# CPython
2020-04-25 11:16:36 +02:00
PythonConfiguration ( version = '2.7.18' , arch = '32' , identifier = 'cp27-win32' , url = None ),
PythonConfiguration ( version = '2.7.18' , arch = '64' , identifier = 'cp27-win_amd64' , url = None ),
2019-12-26 20:30:58 +01:00
PythonConfiguration ( version = '3.5.4' , arch = '32' , identifier = 'cp35-win32' , url = None ),
PythonConfiguration ( version = '3.5.4' , arch = '64' , identifier = 'cp35-win_amd64' , url = None ),
PythonConfiguration ( version = '3.6.8' , arch = '32' , identifier = 'cp36-win32' , url = None ),
PythonConfiguration ( version = '3.6.8' , arch = '64' , identifier = 'cp36-win_amd64' , url = None ),
2020-04-25 13:07:57 +02:00
PythonConfiguration ( version = '3.7.7' , arch = '32' , identifier = 'cp37-win32' , url = None ),
PythonConfiguration ( version = '3.7.7' , arch = '64' , identifier = 'cp37-win_amd64' , url = None ),
2020-03-07 11:37:11 +01:00
PythonConfiguration ( version = '3.8.2' , arch = '32' , identifier = 'cp38-win32' , url = None ),
PythonConfiguration ( version = '3.8.2' , arch = '64' , identifier = 'cp38-win_amd64' , url = None ),
2020-03-18 10:11:09 +00:00
# PyPy
PythonConfiguration ( version = '2.7' , arch = '32' , identifier = 'pp27-win32' , url = 'https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.3.0-win32.zip' ),
PythonConfiguration ( version = '3.6' , arch = '32' , identifier = 'pp36-win32' , url = 'https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.3.0-win32.zip' ),
2019-04-20 18:52:01 +02:00
]
2019-10-23 22:53:10 +02:00
if IS_RUNNING_ON_TRAVIS :
# cannot install VCForPython27.msi which is needed for compiling C software
# try with (and similar): msiexec /i VCForPython27.msi ALLUSERS=1 ACCEPT=YES /passive
2019-11-07 00:55:58 +01:00
python_configurations = [ c for c in python_configurations if not c . version . startswith ( '2.7' )]
2019-10-23 22:53:10 +02:00
2019-11-12 23:51:27 +00:00
# skip builds as required
2019-10-23 22:53:10 +02:00
python_configurations = [ c for c in python_configurations if build_selector ( c . identifier )]
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-02-11 14:44:53 +01:00
def extract_zip ( zip_src , dest ):
with ZipFile ( zip_src ) as zip :
zip . extractall ( dest )
def install_cpython ( version , arch , nuget ):
nuget_args = get_nuget_args ( version , arch )
installation_path = os . path . join ( nuget_args [ - 1 ], nuget_args [ 0 ] + '.' + version , 'tools' )
2020-03-06 10:14:54 +00:00
shell ([ nuget , 'install' ] + nuget_args )
2020-02-11 14:44:53 +01:00
return installation_path
def install_pypy ( version , arch , url ):
assert arch == '32'
# Inside the PyPy zip file is a directory with the same name
zip_filename = url . rsplit ( '/' , 1 )[ - 1 ]
installation_path = os . path . join ( 'C: \\ cibw' , os . path . splitext ( zip_filename )[ 0 ])
if not os . path . exists ( installation_path ):
pypy_zip = os . path . join ( 'C: \\ cibw' , zip_filename )
download ( url , pypy_zip )
# Extract to the parent directory because the zip file still contains a directory
extract_zip ( pypy_zip , os . path . dirname ( installation_path ))
pypy_exe = 'pypy3.exe' if version [ 0 ] == '3' else 'pypy.exe'
2020-03-06 10:14:54 +00:00
shell ([ 'mklink' , os . path . join ( installation_path , 'python.exe' ), os . path . join ( installation_path , pypy_exe )])
shell ([ 'mklink' , '/d' , os . path . join ( installation_path , 'Scripts' ), os . path . join ( installation_path , 'bin' )])
2020-02-11 14:44:53 +01:00
return installation_path
2020-04-06 16:55:15 +02:00
def setup_python ( python_configuration , dependency_constraint_flags , environment ):
2020-04-05 16:03:35 +02:00
nuget = 'C: \\ cibw \\ nuget.exe'
if not os . path . exists ( nuget ):
download ( 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' , nuget )
if python_configuration . identifier . startswith ( 'cp' ):
installation_path = install_cpython ( python_configuration . version , python_configuration . arch , nuget )
elif python_configuration . identifier . startswith ( 'pp' ):
installation_path = install_pypy ( python_configuration . version , python_configuration . arch , python_configuration . url )
else :
raise ValueError ( "Unknown Python implementation" )
assert os . path . exists ( os . path . join ( installation_path , 'python.exe' ))
# set up PATH and environment variables for run_with_env
env = os . environ . copy ()
env [ 'PYTHON_VERSION' ] = python_configuration . version
env [ 'PYTHON_ARCH' ] = python_configuration . arch
env [ 'PATH' ] = os . pathsep . join ([
installation_path ,
os . path . join ( installation_path , 'Scripts' ),
env [ 'PATH' ]
])
# 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
shell ([ 'where' , 'python' ], env = env )
shell ([ 'python' , '--version' ], env = env )
shell ([ 'python' , '-c' , '"import struct; print(struct.calcsize( \' P \' ) * 8)"' ], env = env )
where_python = subprocess . check_output ([ 'where' , 'python' ], env = env , universal_newlines = True ) . splitlines ()[ 0 ] . strip ()
if where_python != os . path . join ( installation_path , 'python.exe' ):
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 )
exit ( 1 )
# make sure pip is installed
if not os . path . exists ( os . path . join ( installation_path , 'Scripts' , 'pip.exe' )):
shell ([ 'python' , get_pip_script ] + dependency_constraint_flags , env = env , cwd = "C: \\ cibw" )
assert os . path . exists ( os . path . join ( installation_path , 'Scripts' , 'pip.exe' ))
where_pip = subprocess . check_output ([ 'where' , 'pip' ], env = env , universal_newlines = True ) . splitlines ()[ 0 ] . strip ()
if where_pip . strip () != os . path . join ( installation_path , 'Scripts' , 'pip.exe' ):
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 )
exit ( 1 )
# prepare the Python environment
shell ([ 'python' , '-m' , 'pip' , 'install' , '--upgrade' , 'pip' ] + dependency_constraint_flags , env = env )
shell ([ 'pip' , '--version' ], env = env )
shell ([ 'pip' , 'install' , '--upgrade' , 'setuptools' , 'wheel' ] + dependency_constraint_flags , env = env )
2020-04-06 16:55:15 +02:00
return env
2020-04-05 16:03:35 +02:00
2020-04-07 00:40:01 +02:00
def build ( options : BuildOptions ):
2017-07-02 18:02:27 -05:00
temp_dir = tempfile . mkdtemp ( prefix = 'cibuildwheel' )
built_wheel_dir = os . path . join ( temp_dir , 'built_wheel' )
2019-11-12 23:34:59 +00:00
repaired_wheel_dir = os . path . join ( temp_dir , 'repaired_wheel' )
2017-07-02 18:02:27 -05:00
2019-10-26 01:09:19 +02:00
# install nuget as best way to provide python
2019-11-09 10:27:16 +01:00
nuget = 'C: \\ cibw \\ nuget.exe'
2019-10-26 01:09:19 +02:00
download ( 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' , nuget )
2019-04-27 10:20:37 +01:00
2020-04-07 00:40:01 +02:00
python_configurations = get_python_configurations ( options . build_selector )
2017-03-19 21:27:29 +00:00
for config in python_configurations :
2020-04-06 16:55:15 +02:00
dependency_constraint_flags = []
2020-04-07 00:40:01 +02:00
if options . dependency_constraints :
2020-04-06 16:55:15 +02:00
dependency_constraint_flags = [
2020-04-07 00:40:01 +02:00
'-c' , options . dependency_constraints . get_for_python_version ( config . version )
2020-04-06 16:55:15 +02:00
]
2019-11-12 11:40:21 +01:00
# install Python
2020-04-07 00:40:01 +02:00
env = setup_python ( config , dependency_constraint_flags , options . environment )
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
# run the before_build command
2020-04-07 00:40:01 +02:00
if options . before_build :
2020-04-09 16:19:36 +01:00
before_build_prepared = prepare_command ( options . before_build , project = '.' , package = options . package_dir )
2017-04-10 21:42:12 +01:00
shell ([ before_build_prepared ], env = env )
2017-03-19 21:27:29 +00:00
# build the wheel
2019-11-12 23:34:59 +00:00
if os . path . exists ( built_wheel_dir ):
shutil . rmtree ( built_wheel_dir )
os . makedirs ( built_wheel_dir )
2020-04-09 16:19:36 +01:00
shell ([ 'pip' , 'wheel' , options . package_dir , '-w' , built_wheel_dir , '--no-deps' ] + get_build_verbosity_extra_flags ( options . build_verbosity ), env = env )
2019-11-12 23:34:59 +00:00
built_wheel = glob ( os . path . join ( built_wheel_dir , '*.whl' ))[ 0 ]
# repair the wheel
if os . path . exists ( repaired_wheel_dir ):
shutil . rmtree ( repaired_wheel_dir )
os . makedirs ( repaired_wheel_dir )
2020-04-07 00:40:01 +02:00
if built_wheel . endswith ( 'none-any.whl' ) or not options . repair_command :
2019-11-12 23:34:59 +00:00
# pure Python wheel or empty repair command
shutil . move ( built_wheel , repaired_wheel_dir )
else :
2020-04-07 00:40:01 +02:00
repair_command_prepared = prepare_command ( options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir )
2019-11-12 23:34:59 +00:00
shell ([ repair_command_prepared ], env = env )
repaired_wheel = glob ( os . path . join ( repaired_wheel_dir , '*.whl' ))[ 0 ]
2017-03-19 21:27:29 +00:00
2020-04-07 00:40:01 +02:00
if options . test_command :
2019-10-12 10:47:00 +01: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.
2020-01-28 22:04:37 +00:00
shell ([ 'pip' , 'install' , 'virtualenv' ] + dependency_constraint_flags , env = env )
2019-10-12 10:47:00 +01:00
venv_dir = tempfile . mkdtemp ()
2020-03-01 11:09:22 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
2020-02-18 22:03:14 +00:00
shell ([ 'python' , '-m' , 'virtualenv' , '--no-download' , venv_dir ], env = env )
2019-10-12 10:47:00 +01:00
virtualenv_env = env . copy ()
2020-03-02 14:54:48 +01:00
venv_script_path = os . path . join ( venv_dir , 'Scripts' )
if os . path . exists ( os . path . join ( venv_dir , 'bin' )):
2020-03-02 14:34:39 +01:00
# pypy2.7 bugfix
2020-03-02 14:54:48 +01:00
venv_script_path = os . pathsep . join ([ venv_script_path , os . path . join ( venv_dir , 'bin' )])
2019-10-12 10:47:00 +01:00
virtualenv_env [ 'PATH' ] = os . pathsep . join ([
2020-03-02 14:54:48 +01:00
venv_script_path ,
2019-10-12 10:47:00 +01:00
virtualenv_env [ 'PATH' ],
])
2020-03-01 01:50:45 +01:00
virtualenv_env [ "__CIBW_VIRTUALENV_PATH__" ] = venv_dir
2019-10-12 10:47:00 +01:00
# check that we are using the Python from the virtual environment
shell ([ 'which' , 'python' ], env = virtualenv_env )
2020-04-07 00:40:01 +02:00
if options . before_test :
2020-04-09 16:19:36 +01:00
before_test_prepared = prepare_command (
options . before_test ,
project = '.' ,
package = options . package_dir
)
2020-01-07 00:17:03 +01:00
shell ([ before_test_prepared ], env = virtualenv_env )
2019-10-12 10:47:00 +01:00
# install the wheel
2020-04-07 00:40:01 +02:00
shell ([ 'pip' , 'install' , repaired_wheel + options . test_extras ], env = virtualenv_env )
2019-10-12 10:47:00 +01:00
# test the wheel
2020-04-07 00:40:01 +02:00
if options . test_requires :
shell ([ 'pip' , 'install' ] + options . test_requires , env = virtualenv_env )
2019-10-12 10:47:00 +01:00
2017-03-19 21:27:29 +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)
2020-04-09 16:19:36 +01:00
test_command_prepared = prepare_command (
options . test_command ,
project = os . path . abspath ( '.' ),
package = os . path . abspath ( options . package_dir )
)
2019-10-12 10:47:00 +01:00
shell ([ test_command_prepared ], cwd = 'c: \\ ' , env = virtualenv_env )
# clean up
shutil . rmtree ( venv_dir )
2017-07-02 18:02:27 -05:00
2018-09-05 11:19:48 +01:00
# we're all done here; move it to output (remove if already exists)
2020-04-07 00:40:01 +02:00
dst = os . path . join ( options . output_dir , os . path . basename ( repaired_wheel ))
2018-09-05 11:19:48 +01:00
if os . path . isfile ( dst ):
os . remove ( dst )
2019-11-12 23:34:59 +00:00
shutil . move ( repaired_wheel , dst )