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
2019-11-07 00:55:58 +01:00
from zipfile import ZipFile
2017-03-19 21:27:29 +00:00
2020-05-07 00:07:14 +02:00
from typing import Dict , List , Optional , NamedTuple
2020-04-08 00:16:25 +02:00
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2019-11-12 23:51:27 +00:00
from .util import (
2020-04-08 00:16:25 +02:00
BuildOptions ,
2020-05-07 00:07:14 +02:00
BuildSelector ,
2019-11-12 23:51:27 +00:00
download ,
get_build_verbosity_extra_flags ,
2020-02-15 11:45:54 +00:00
get_pip_script ,
2020-04-08 00:16:25 +02:00
prepare_command ,
2019-11-12 23:51:27 +00:00
)
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2020-06-15 01:53:31 +02:00
IS_RUNNING_ON_AZURE = Path ( 'C: \\ hostedtoolcache' ) . exists ()
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-04-08 00:16:25 +02:00
def shell ( args : List [ str ], env : Optional [ Dict [ str , str ]] = None , cwd : Optional [ str ] = None ) -> int :
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
2020-04-08 00:16:25 +02:00
def get_nuget_args ( version : str , arch : str ) -> List [ str ]:
2019-12-26 20:30:58 +01:00
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
2020-04-10 01:53:06 +02:00
class PythonConfiguration ( NamedTuple ):
version : str
arch : str
identifier : str
url : Optional [ str ]
2020-04-08 00:16:25 +02:00
2020-05-07 00:07:14 +02:00
def get_python_configurations ( build_selector : BuildSelector ) -> List [ PythonConfiguration ]:
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-05-15 13:35:05 +02:00
PythonConfiguration ( version = '3.8.3' , arch = '32' , identifier = 'cp38-win32' , url = None ),
PythonConfiguration ( version = '3.8.3' , arch = '64' , identifier = 'cp38-win_amd64' , url = None ),
2020-03-18 10:11:09 +00:00
# PyPy
2020-04-28 20:05:28 +02:00
PythonConfiguration ( version = '2.7' , arch = '32' , identifier = 'pp27-win32' , url = 'https://downloads.python.org/pypy/pypy2.7-v7.3.1-win32.zip' ),
PythonConfiguration ( version = '3.6' , arch = '32' , identifier = 'pp36-win32' , url = 'https://downloads.python.org/pypy/pypy3.6-v7.3.1-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-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 )
2020-06-15 01:53:31 +02:00
installation_path = Path ( nuget_args [ - 1 ]) / ( nuget_args [ 0 ] + '.' + version ) / 'tools'
shell ([ str ( 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 :
2020-02-11 14:44:53 +01:00
assert arch == '32'
# Inside the PyPy zip file is a directory with the same name
zip_filename = url . rsplit ( '/' , 1 )[ - 1 ]
2020-06-15 01:53:31 +02:00
extension = ".zip"
assert zip_filename . endswith ( extension )
installation_path = Path ( 'C: \\ cibw' ) / zip_filename [: - len ( extension )]
if not installation_path . exists ():
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 )
2020-02-11 14:44:53 +01:00
pypy_exe = 'pypy3.exe' if version [ 0 ] == '3' else 'pypy.exe'
2020-06-15 01:53:31 +02:00
( installation_path / pypy_exe ) . symlink_to ( installation_path / 'python.exe' )
2020-02-11 14:44:53 +01:00
return installation_path
2020-04-10 01:44:02 +02:00
def setup_python ( python_configuration : PythonConfiguration , dependency_constraint_flags : List [ str ], environment : ParsedEnvironment ) -> Dict [ str , str ]:
2020-06-15 01:53:31 +02:00
nuget = Path ( 'C: \\ cibw \\ nuget.exe' )
if not nuget . exists ():
2020-04-05 16:03:35 +02:00
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' ):
2020-04-10 20:24:52 +02:00
assert python_configuration . url is not None
2020-04-05 16:03:35 +02:00
installation_path = install_pypy ( python_configuration . version , python_configuration . arch , python_configuration . url )
else :
raise ValueError ( "Unknown Python implementation" )
2020-06-15 01:53:31 +02:00
assert ( installation_path / 'python.exe' ) . exists ()
2020-04-05 16:03:35 +02:00
# 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 ([
2020-06-15 01:53:31 +02:00
str ( installation_path ),
str ( installation_path / 'Scripts' ),
2020-04-05 16:03:35 +02:00
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 ()
2020-06-15 01:53:31 +02:00
if where_python != str ( installation_path / 'python.exe' ):
2020-04-05 16:03:35 +02: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 )
exit ( 1 )
# make sure pip is installed
2020-06-15 01:53:31 +02:00
if not ( installation_path / 'Scripts' / 'pip.exe' ) . exists ():
shell ([ 'python' , str ( get_pip_script )] + dependency_constraint_flags , env = env , cwd = "C: \\ cibw" )
assert ( installation_path / 'Scripts' / 'pip.exe' ) . exists ()
2020-04-05 16:03:35 +02:00
where_pip = subprocess . check_output ([ 'where' , 'pip' ], env = env , universal_newlines = True ) . splitlines ()[ 0 ] . strip ()
2020-06-15 01:53:31 +02:00
if where_pip . strip () != str ( installation_path / 'Scripts' / 'pip.exe' ):
2020-04-05 16:03:35 +02: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 )
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-10 01:44:02 +02:00
def build ( options : BuildOptions ) -> None :
2020-06-15 01:53:31 +02: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
2019-10-26 01:09:19 +02:00
# install nuget as best way to provide python
2020-06-15 01:53:31 +02:00
nuget = Path ( '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-06-15 01:53:31 +02:00
'-c' , str ( 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
2020-06-15 01:53:31 +02:00
if built_wheel_dir . exists ():
2019-11-12 23:34:59 +00:00
shutil . rmtree ( built_wheel_dir )
2020-06-15 01:53:31 +02:00
built_wheel_dir . mkdir ( parents = True )
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
2020-06-08 10:19:02 +02:00
# see https://github.com/joerick/cibuildwheel/pull/369
2020-06-15 01:53:31 +02:00
shell ([ 'pip' , 'wheel' , str ( options . package_dir . resolve ()), '-w' , str ( built_wheel_dir ), '--no-deps' ] + get_build_verbosity_extra_flags ( options . build_verbosity ), env = env )
built_wheel = next ( built_wheel_dir . glob ( '*.whl' ))
2019-11-12 23:34:59 +00:00
# repair the wheel
2020-06-15 01:53:31 +02:00
if repaired_wheel_dir . exists ():
2019-11-12 23:34:59 +00:00
shutil . rmtree ( repaired_wheel_dir )
2020-06-15 01:53:31 +02:00
repaired_wheel_dir . mkdir ( parents = True )
if built_wheel . name . endswith ( 'none-any.whl' ) or not options . repair_command :
2019-11-12 23:34:59 +00:00
# pure Python wheel or empty repair command
2020-06-15 01:53:31 +02:00
shutil . move ( str ( built_wheel ), repaired_wheel_dir )
2019-11-12 23:34:59 +00:00
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 )
2020-06-15 01:53:31 +02:00
repaired_wheel = next ( repaired_wheel_dir . glob ( '*.whl' ))
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 )
2020-06-15 01:53:31 +02:00
venv_dir = Path ( tempfile . mkdtemp ())
2020-03-01 11:09:22 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
2020-06-15 01:53:31 +02:00
shell ([ 'python' , '-m' , 'virtualenv' , '--no-download' , str ( venv_dir )], env = env )
2019-10-12 10:47:00 +01:00
virtualenv_env = env . copy ()
virtualenv_env [ 'PATH' ] = os . pathsep . join ([
2020-06-15 01:53:31 +02:00
str ( venv_dir / 'Scripts' ),
2019-10-12 10:47:00 +01:00
virtualenv_env [ 'PATH' ],
])
# 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-06-15 01:53:31 +02:00
shell ([ 'pip' , 'install' , str ( 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 ,
2020-06-15 01:53:31 +02:00
project = Path ( '.' ) . resolve (),
package = options . package_dir . resolve ()
2020-04-09 16:19:36 +01:00
)
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-06-15 01:53:31 +02:00
dst = options . output_dir / repaired_wheel . name
if dst . is_file ():
dst . unlink ()
shutil . move ( str ( repaired_wheel ), dst )