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-01-01 16:53:45 -05: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
2020-05-29 17:24:01 -04:00
import toml
2017-03-19 21:27:29 +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-01-02 14:32:55 -05: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 ,
get_pip_script ,
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
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
2021-01-02 14:32:55 -05:00
def call ( args : Sequence [ PathOrStr ], env : Optional [ Dict [ str , str ]] = None ,
2020-07-08 18:17:55 +01:00
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
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 (
build_selector : BuildSelector ,
architectures : Set [ Architecture ],
) -> List [ PythonConfiguration ]:
full_python_configs = read_python_configs ( 'windows' )
python_configurations = [ PythonConfiguration ( ** item ) for item in full_python_configs ]
2021-01-01 16:53:45 -05:00
map_arch = {
'32' : Architecture . x86 ,
'64' : Architecture . AMD64 ,
}
2021-01-18 16:44:47 -05:00
custom_compiler = os . environ . get ( 'DISTUTILS_USE_SDK' ) and os . environ . get ( 'MSSdk' )
if IS_RUNNING_ON_TRAVIS and not custom_compiler :
2019-10-23 22:53:10 +02:00
# 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
2021-01-01 16:53:45 -05:00
python_configurations = [
c for c in python_configurations
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 )
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
2021-01-02 14:32:55 -05:00
def setup_python ( python_configuration : PythonConfiguration , dependency_constraint_flags : Sequence [ PathOrStr ], environment : ParsedEnvironment ) -> Dict [ str , str ]:
2020-06-15 01:53:31 +02:00
nuget = Path ( 'C: \\ cibw \\ nuget.exe' )
if not nuget . exists ():
2020-11-13 16:30:27 +00:00
log . step ( 'Downloading nuget...' )
2020-04-05 16:03:35 +02:00
download ( 'https://dist.nuget.org/win-x86-commandline/latest/nuget.exe' , nuget )
2020-11-13 16:30:27 +00:00
implementation_id = python_configuration . identifier . split ( "-" )[ 0 ]
log . step ( f 'Installing Python { implementation_id } ...' )
if implementation_id . startswith ( 'cp' ):
2020-04-05 16:03:35 +02:00
installation_path = install_cpython ( python_configuration . version , python_configuration . arch , nuget )
2020-11-13 16:30:27 +00:00
elif implementation_id . 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
2020-11-13 16:30:27 +00:00
log . step ( 'Setting up build environment...' )
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 )
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-04-05 16:03:35 +02:00
# 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 )
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-04-05 16:03:35 +02:00
2020-11-13 16:30:27 +00:00
log . step ( 'Installing build tools...' )
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-11-13 16:30:27 +00:00
log . step ( 'Performing PEP518 workaround...' )
2020-08-14 14:55:08 -04:00
with tempfile . TemporaryDirectory () as d :
reqfile = Path ( d ) / "requirements.txt"
2021-01-09 15:40:40 -05:00
with reqfile . open ( 'w' ) as f :
2020-08-14 14:55:08 -04:00
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
2020-11-13 16:30:27 +00:00
try :
if options . before_all :
log . step ( 'Running before_all...' )
env = options . environment . as_dictionary ( prev_environment = os . environ )
before_all_prepared = prepare_command ( options . before_all , project = '.' , package = options . package_dir )
shell ( before_all_prepared , env = env )
2019-04-27 10:20:37 +01:00
2021-01-01 16:53:45 -05: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 = [
'-c' , options . dependency_constraints . get_for_python_version ( config . version )
]
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 :
log . step ( 'Running before_build...' )
before_build_prepared = prepare_command ( options . before_build , project = '.' , package = options . package_dir )
shell ( before_build_prepared , env = env )
2020-06-14 11:25:14 -04:00
2020-11-13 16:30:27 +00: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 )
2020-06-23 20:28:54 +01:00
2020-11-13 16:30:27 +00:00
log . step ( 'Building wheel...' )
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
call ([
'pip' , 'wheel' ,
options . package_dir . resolve (),
'-w' , built_wheel_dir ,
'--no-deps' ,
* get_build_verbosity_extra_flags ( options . build_verbosity )
], env = env )
2019-11-12 23:34:59 +00:00
2020-11-13 16:30:27 +00: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
2020-11-13 16:30:27 +00:00
if built_wheel . name . endswith ( 'none-any.whl' ):
raise NonPlatformWheelError ()
2020-07-20 15:35:51 +01:00
2020-11-13 16:30:27 +00:00
if options . repair_command :
log . step ( 'Repairing wheel...' )
repair_command_prepared = prepare_command ( options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir )
shell ( repair_command_prepared , env = env )
else :
shutil . move ( str ( built_wheel ), repaired_wheel_dir )
2017-03-19 21:27:29 +00:00
2020-11-13 16:30:27 +00: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 ):
2020-11-13 16:30:27 +00:00
log . step ( 'Testing wheel...' )
# set up a virtual environment to install and test from, to make sure
# there are no dependencies that were pulled in at build time.
call ([ 'pip' , 'install' , 'virtualenv' , * dependency_constraint_flags ], env = env )
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
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 ()
virtualenv_env [ 'PATH' ] = os . pathsep . join ([
str ( venv_dir / 'Scripts' ),
virtualenv_env [ 'PATH' ],
])
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
call ([ 'which' , 'python' ], env = virtualenv_env )
if options . before_test :
before_test_prepared = prepare_command (
options . before_test ,
project = '.' ,
package = options . package_dir
)
shell ( before_test_prepared , env = virtualenv_env )
# install the wheel
call ([ 'pip' , 'install' , str ( repaired_wheel ) + options . test_extras ], env = virtualenv_env )
# test the wheel
if options . test_requires :
call ([ 'pip' , 'install' ] + options . test_requires , env = virtualenv_env )
# 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 ,
project = Path ( '.' ) . resolve (),
package = options . package_dir . resolve ()
2020-04-09 16:19:36 +01:00
)
2020-11-13 16:30:27 +00: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-01-05 13:50:21 +00:00
log . step_end_with_error ( f 'Command { error . cmd } failed with code { error . returncode } . { error . stdout } ' )
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )