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-23 20:28:54 +01:00
from os import PathLike
2020-06-15 01:53:31 +02:00
from pathlib import Path
2020-06-23 20:28:54 +01:00
from typing import Dict , List , NamedTuple , Optional , Sequence , Union
2019-11-07 00:55:58 +01:00
from zipfile import ZipFile
2020-05-29 17:24:01 -04:00
import toml
2017-03-19 21:27:29 +00:00
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2020-07-20 15:35:51 +01:00
from .util import ( BuildOptions , BuildSelector , NonPlatformWheelError , download ,
2020-06-23 20:28:54 +01:00
get_build_verbosity_extra_flags , get_pip_script ,
prepare_command )
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-07-08 18:17:55 +01:00
def call ( args : Sequence [ Union [ str , PathLike ]], env : Optional [ Dict [ str , str ]] = None ,
cwd : Optional [ str ] = None ) -> None :
print ( '+ ' + ' ' . join ( str ( a ) for a in args ))
# we use shell=True here, even though we don't need a shell due to a bug
# https://bugs.python.org/issue8557
subprocess . check_call ([ str ( a ) for a in args ], env = env , cwd = cwd , shell = True )
def shell ( command : str , env : Optional [ Dict [ str , str ]] = None , cwd : Optional [ str ] = None ) -> None :
2020-06-23 20:28:54 +01:00
print ( f '+ { command } ' )
2020-07-08 18:17:55 +01:00
subprocess . check_call ( command , 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-07-04 18:11:06 +02:00
PythonConfiguration ( version = '3.7.8' , arch = '32' , identifier = 'cp37-win32' , url = None ),
PythonConfiguration ( version = '3.7.8' , arch = '64' , identifier = 'cp37-win_amd64' , url = None ),
2020-07-18 11:05:23 +02:00
PythonConfiguration ( version = '3.8.4' , arch = '32' , identifier = 'cp38-win32' , url = None ),
PythonConfiguration ( version = '3.8.4' , arch = '64' , identifier = 'cp38-win_amd64' , url = None ),
2020-10-06 22:02:11 -04:00
PythonConfiguration ( version = '3.9.0' , arch = '32' , identifier = 'cp39-win32' , url = None ),
PythonConfiguration ( version = '3.9.0' , arch = '64' , identifier = 'cp39-win_amd64' , url = None ),
2020-03-18 10:11:09 +00:00
# PyPy
2020-09-25 15:27:04 +02:00
PythonConfiguration ( version = '2.7' , arch = '32' , identifier = 'pp27-win32' , url = 'https://downloads.python.org/pypy/pypy2.7-v7.3.2-win32.zip' ),
PythonConfiguration ( version = '3.6' , arch = '32' , identifier = 'pp36-win32' , url = 'https://downloads.python.org/pypy/pypy3.6-v7.3.2-win32.zip' ),
PythonConfiguration ( version = '3.7' , arch = '32' , identifier = 'pp37-win32' , url = 'https://downloads.python.org/pypy/pypy3.7-v7.3.2-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'
2020-07-08 18:17:55 +01:00
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 :
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-16 15:18:32 +02:00
( installation_path / 'python.exe' ) . symlink_to ( installation_path / pypy_exe )
2020-02-11 14:44:53 +01:00
return installation_path
2020-06-23 20:28:54 +01:00
def setup_python ( python_configuration : PythonConfiguration , dependency_constraint_flags : Sequence [ Union [ str , PathLike ]], 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
2020-07-08 18:17:55 +01:00
call ([ 'where' , 'python' ], env = env )
call ([ 'python' , '--version' ], env = env )
call ([ 'python' , '-c' , '"import struct; print(struct.calcsize( \' P \' ) * 8)"' ], env = env )
2020-04-05 16:03:35 +02:00
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 ():
2020-07-08 18:17:55 +01:00
call ([ 'python' , get_pip_script , * dependency_constraint_flags ], env = env , cwd = "C: \\ cibw" )
2020-06-15 01:53:31 +02:00
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
2020-07-08 18:17:55 +01:00
call ([ 'python' , '-m' , 'pip' , 'install' , '--upgrade' , 'pip' , * dependency_constraint_flags ], env = env )
call ([ 'pip' , '--version' ], env = env )
call ([ 'pip' , 'install' , '--upgrade' , 'setuptools' , 'wheel' , * dependency_constraint_flags ], env = env )
2020-04-05 16:03:35 +02:00
2020-06-14 11:25:14 -04:00
return env
2020-06-22 12:08:40 +01:00
def pep_518_cp35_workaround ( package_dir : Path , env : Dict [ str , str ]) -> None :
2020-06-14 11:25:14 -04:00
"""
Python 3.5 PEP 518 hack (see https://github.com/pypa/pip/issues/8392#issuecomment-639563494)
Basically, nuget's Python is an embedded Python distribution, which is not supported by pip.
Before version 3.6, there was no way to disable the "embedded" behavior, including the ignoring
of environment variables, including the ones pip uses to setup PEP 518 builds.
The fix here is as suggested in that issue; we manually setup the PEP 518 requirements. Since we
are in a fresh environment (except for pinned cibuildweel dependencies), the build is already
mostly "isolated".
"""
2020-06-22 12:08:40 +01:00
pyproject_path = package_dir / 'pyproject.toml'
2020-06-14 11:25:14 -04:00
2020-06-22 12:08:40 +01:00
if pyproject_path . exists ():
2020-06-14 11:25:14 -04:00
data = toml . load ( pyproject_path )
2020-05-29 17:24:01 -04:00
requirements = (
data [ 'build-system' ] . get ( 'requires' , [])
if 'build-system' in data
else []
)
2020-08-13 20:50:36 -04:00
2020-05-29 17:24:01 -04:00
if requirements :
2020-08-14 14:55:08 -04:00
with tempfile . TemporaryDirectory () as d :
reqfile = Path ( d ) / "requirements.txt"
with reqfile . open ( "w" ) as f :
for r in requirements :
print ( r , file = f )
2020-08-16 14:05:39 -04:00
call ([ 'pip' , 'install' , '-r' , reqfile ], env = 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-05-11 17:09:54 +02:00
if options . before_all :
2020-05-15 13:13:13 +02:00
env = options . environment . as_dictionary ( prev_environment = os . environ )
2020-05-11 17:09:54 +02:00
before_all_prepared = prepare_command ( options . before_all , project = '.' , package = options . package_dir )
2020-07-08 18:17:55 +01:00
shell ( before_all_prepared , env = env )
2020-05-11 17:09:54 +02: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-06-23 20:28:54 +01:00
dependency_constraint_flags : Sequence [ Union [ str , PathLike ]] = []
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-23 20:28:54 +01: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 )
2020-07-08 18:17:55 +01:00
shell ( before_build_prepared , env = env )
2017-04-10 21:42:12 +01:00
2020-06-14 11:25:14 -04:00
# activate the PEP 518 patch if on Windows Python 3.5
# (will only have an effect if PEP 517 builds are used):
if config . version . startswith ( '3.5' ):
pep_518_cp35_workaround ( options . package_dir , 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-07-08 18:17:55 +01:00
call ([
2020-06-25 12:41:21 +01:00
'pip' , 'wheel' ,
options . package_dir . resolve (),
'-w' , built_wheel_dir ,
'--no-deps' ,
* get_build_verbosity_extra_flags ( options . build_verbosity )
], env = env )
2020-06-23 20:28:54 +01:00
2020-06-15 01:53:31 +02:00
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 )
2020-07-20 15:35:51 +01:00
if built_wheel . name . endswith ( 'none-any.whl' ):
raise NonPlatformWheelError ()
if options . repair_command :
2020-04-07 00:40:01 +02:00
repair_command_prepared = prepare_command ( options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir )
2020-07-08 18:17:55 +01:00
shell ( repair_command_prepared , env = env )
2020-07-20 15:35:51 +01:00
else :
shutil . move ( str ( built_wheel ), repaired_wheel_dir )
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-07-08 18:17:55 +01:00
call ([ '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-07-08 18:17:55 +01:00
call ([ 'python' , '-m' , 'virtualenv' , '--no-download' , 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
2020-07-08 18:17:55 +01:00
call ([ 'which' , 'python' ], env = virtualenv_env )
2019-10-12 10:47:00 +01:00
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-07-08 18:17:55 +01:00
shell ( before_test_prepared , env = virtualenv_env )
2020-01-07 00:17:03 +01:00
2019-10-12 10:47:00 +01:00
# install the wheel
2020-07-08 18:17:55 +01:00
call ([ '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 :
2020-07-08 18:17:55 +01:00
call ([ '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
)
2020-07-08 18:17:55 +01:00
shell ( test_command_prepared , cwd = 'c: \\ ' , env = virtualenv_env )
2019-10-12 10:47:00 +01:00
# 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-24 23:04:15 +00:00
shutil . move ( str ( repaired_wheel ), options . output_dir )