2019-11-12 23:51:27 +00:00
import os
import shlex
import shutil
import subprocess
2020-02-19 17:56:11 +01:00
import sys
2019-09-07 20:20:31 +01:00
import tempfile
2017-03-19 21:27:29 +00:00
from collections import namedtuple
from glob import glob
2019-11-12 23:51:27 +00:00
from .util import (
download ,
get_build_verbosity_extra_flags ,
prepare_command ,
)
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2020-02-11 14:44:53 +01:00
def call ( args , env = None , cwd = None , shell = False ):
# print the command executing for the logs
if shell :
print ( '+ %s ' % args )
else :
print ( '+ ' + ' ' . join ( shlex . quote ( a ) for a in args ))
return subprocess . check_call ( args , env = env , cwd = cwd , shell = shell )
2019-04-20 18:52:01 +02:00
def get_python_configurations ( build_selector ):
2017-04-11 23:15:19 +01:00
PythonConfiguration = namedtuple ( 'PythonConfiguration' , [ 'version' , 'identifier' , 'url' ])
2017-03-19 21:27:29 +00:00
python_configurations = [
2019-11-19 23:36:20 +01:00
PythonConfiguration ( version = '2.7' , identifier = 'cp27-macosx_x86_64' , url = 'https://www.python.org/ftp/python/2.7.17/python-2.7.17-macosx10.9.pkg' ),
PythonConfiguration ( version = '3.5' , identifier = 'cp35-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.5.4/python-3.5.4-macosx10.6.pkg' ),
PythonConfiguration ( version = '3.6' , identifier = 'cp36-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.6.8/python-3.6.8-macosx10.9.pkg' ),
PythonConfiguration ( version = '3.7' , identifier = 'cp37-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.7.6/python-3.7.6-macosx10.9.pkg' ),
PythonConfiguration ( version = '3.8' , identifier = 'cp38-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.8.1/python-3.8.1-macosx10.9.pkg' ),
2020-02-13 10:52:46 +01:00
PythonConfiguration ( version = '2.7-v7.3.0' , identifier = 'pp27-macosx_x86_64' , url = 'https://bitbucket.org/pypy/pypy/downloads/pypy2.7-v7.3.0-osx64.tar.bz2' ),
PythonConfiguration ( version = '3.6-v7.3.0' , identifier = 'pp36-macosx_x86_64' , url = 'https://bitbucket.org/pypy/pypy/downloads/pypy3.6-v7.3.0-osx64.tar.bz2' ),
2017-03-19 21:27:29 +00:00
]
2019-04-20 18:52:01 +02:00
# skip builds as required
return [ c for c in python_configurations if build_selector ( c . identifier )]
2020-02-19 17:09:18 +01:00
SYMLINKS_DIR = '/tmp/cibw_bin'
def make_symlinks ( installation_bin_path , python_executable , pip_executable ):
2020-02-16 14:32:19 +01:00
assert os . path . exists ( os . path . join ( installation_bin_path , python_executable ))
# Python bin folders on Mac don't symlink `python3` to `python`, and neither
# does PyPy for `pypy` or `pypy3`, so we do that so `python` and `pip` always
# point to the active configuration.
2020-02-19 17:09:18 +01:00
if os . path . exists ( SYMLINKS_DIR ):
shutil . rmtree ( SYMLINKS_DIR )
os . makedirs ( SYMLINKS_DIR )
2020-02-16 14:32:19 +01:00
2020-02-19 17:09:18 +01:00
os . symlink ( os . path . join ( installation_bin_path , python_executable ), os . path . join ( SYMLINKS_DIR , 'python' ))
os . symlink ( os . path . join ( installation_bin_path , python_executable + '-config' ), os . path . join ( SYMLINKS_DIR , 'python-config' ))
os . symlink ( os . path . join ( installation_bin_path , pip_executable ), os . path . join ( SYMLINKS_DIR , 'pip' ))
2020-02-16 14:32:19 +01:00
2020-02-19 17:09:18 +01:00
def install_cpython ( version , url ):
2020-02-11 14:44:53 +01:00
installed_system_packages = subprocess . check_output ([ 'pkgutil' , '--pkgs' ], universal_newlines = True ) . splitlines ()
# if this version of python isn't installed, get it from python.org and install
python_package_identifier = 'org.python.Python.PythonFramework- {} ' . format ( version )
if python_package_identifier not in installed_system_packages :
# download the pkg
download ( url , '/tmp/Python.pkg' )
# install
call ([ 'sudo' , 'installer' , '-pkg' , '/tmp/Python.pkg' , '-target' , '/' ])
# patch open ssl
if version == '3.5' :
open_ssl_patch_url = 'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.0.2t/patch-macos-python- %s -openssl-v1.0.2t.tar.gz' % version
download ( open_ssl_patch_url , '/tmp/python-patch.tar.gz' )
call ([ 'sudo' , 'tar' , '-C' , '/Library/Frameworks/Python.framework/Versions/ {} /' . format ( version ), '-xmf' , '/tmp/python-patch.tar.gz' ])
2020-02-16 14:32:19 +01:00
installation_bin_path = '/Library/Frameworks/Python.framework/Versions/ {} /bin' . format ( version )
python_executable = 'python3' if version [ 0 ] == '3' else 'python'
pip_executable = 'pip3' if version [ 0 ] == '3' else 'pip'
2020-02-19 17:09:18 +01:00
make_symlinks ( installation_bin_path , python_executable , pip_executable )
2020-02-16 14:32:19 +01:00
return installation_bin_path
2020-02-11 14:44:53 +01:00
2020-02-19 17:09:18 +01:00
def install_pypy ( version , url ):
2020-02-11 14:44:53 +01:00
pypy_tar_bz2 = url . rsplit ( '/' , 1 )[ - 1 ]
assert pypy_tar_bz2 . endswith ( ".tar.bz2" )
pypy_base_filename = os . path . splitext ( os . path . splitext ( pypy_tar_bz2 )[ 0 ])[ 0 ]
installation_path = os . path . join ( '/tmp' , pypy_base_filename )
if not os . path . exists ( installation_path ):
download ( url , os . path . join ( "/tmp" , pypy_tar_bz2 ))
call ([ 'tar' , '-C' , '/tmp' , '-xf' , os . path . join ( "/tmp" , pypy_tar_bz2 )])
2020-02-16 14:32:19 +01:00
2020-02-19 07:45:13 +01:00
# fix PyPy 7.3.0 bug resulting in wrong macOS platform tag
if version . endswith ( "-v7.3.0" ) and version [ 0 ] == '3' :
patch_file = os . path . abspath ( os . path . join ( os . path . dirname ( __file__ ), 'resources' , 'pypy3.6.patch' ))
sysconfigdata_file = os . path . join ( installation_path , 'lib_pypy' , '_sysconfigdata.py' )
call ([ 'patch' , sysconfigdata_file , patch_file , '-N' ]) # Always has nonzero return code
2020-02-16 14:32:19 +01:00
installation_bin_path = os . path . join ( installation_path , 'bin' )
python_executable = 'pypy3' if version [ 0 ] == '3' else 'pypy'
pip_executable = 'pip3' if version [ 0 ] == '3' else 'pip'
2020-02-19 17:09:18 +01:00
make_symlinks ( installation_bin_path , python_executable , pip_executable )
2020-02-16 14:32:19 +01:00
return installation_bin_path
2020-02-11 14:44:53 +01:00
2019-11-12 23:34:59 +00:00
def build ( project_dir , output_dir , test_command , test_requires , test_extras , before_build , build_verbosity , build_selector , repair_command , environment ):
abs_project_dir = os . path . abspath ( project_dir )
temp_dir = tempfile . mkdtemp ( prefix = 'cibuildwheel' )
built_wheel_dir = os . path . join ( temp_dir , 'built_wheel' )
repaired_wheel_dir = os . path . join ( temp_dir , 'repaired_wheel' )
2019-04-20 18:52:01 +02:00
python_configurations = get_python_configurations ( build_selector )
2019-11-19 23:36:20 +01:00
2018-03-31 13:02:42 +02:00
get_pip_url = 'https://bootstrap.pypa.io/get-pip.py'
get_pip_script = '/tmp/get-pip.py'
2017-03-19 21:27:29 +00:00
2018-03-31 13:02:42 +02:00
# get latest pip once and for all
2020-01-31 17:06:58 +00:00
download ( get_pip_url , get_pip_script )
2018-03-31 13:02:42 +02:00
2017-03-19 21:27:29 +00:00
for config in python_configurations :
2019-11-17 17:56:07 +01:00
if config . identifier . startswith ( 'cp' ):
2020-02-19 17:09:18 +01:00
installation_bin_path = install_cpython ( config . version , config . url )
2019-11-17 17:56:07 +01:00
elif config . identifier . startswith ( 'pp' ):
2020-02-19 17:09:18 +01:00
installation_bin_path = install_pypy ( config . version , config . url )
2019-12-26 20:30:58 +01:00
else :
raise ValueError ( "Unknown Python implementation" )
2019-11-17 17:56:07 +01:00
2017-03-19 21:27:29 +00:00
env = os . environ . copy ()
env [ 'PATH' ] = os . pathsep . join ([
2020-02-19 17:09:18 +01:00
SYMLINKS_DIR ,
2018-04-08 15:05:21 +02:00
installation_bin_path ,
2017-03-19 21:27:29 +00:00
env [ 'PATH' ],
])
2017-09-01 13:33:29 +01:00
env = environment . as_dictionary ( prev_environment = env )
2017-03-19 21:27:29 +00:00
# check what version we're on
2018-04-08 13:40:30 +02:00
call ([ 'which' , 'python' ], env = env )
call ([ 'python' , '--version' ], env = env )
2020-02-19 17:56:11 +01:00
which_python = subprocess . check_output ([ 'which' , 'python' ], env = env , universal_newlines = True ) . strip ()
if which_python != '/tmp/cibw_bin/python' :
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 )
2017-03-19 21:27:29 +00:00
# install pip & wheel
2019-11-17 17:56:07 +01:00
call ([ 'python' , get_pip_script ], env = env , cwd = "/tmp" )
2019-11-12 11:40:21 +01:00
assert os . path . exists ( os . path . join ( installation_bin_path , 'pip' ))
2020-02-19 17:56:11 +01:00
call ([ 'which' , 'pip' ], env = env )
2018-04-08 13:40:30 +02:00
call ([ 'pip' , '--version' ], env = env )
2020-02-19 17:56:11 +01:00
which_pip = subprocess . check_output ([ 'which' , 'pip' ], env = env , universal_newlines = True ) . strip ()
if which_pip != '/tmp/cibw_bin/pip' :
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 )
2020-01-27 09:36:41 +01:00
call ([ 'pip' , 'install' , '--upgrade' , 'setuptools' , 'wheel' , 'delocate' ], env = env )
2017-03-19 21:27:29 +00:00
2019-11-19 23:36:20 +01:00
# setup target platform, only required for python 3.5
if config . version == '3.5' :
2020-01-31 17:05:32 +00:00
if '_PYTHON_HOST_PLATFORM' not in env :
# cross-compilation platform override
env [ '_PYTHON_HOST_PLATFORM' ] = 'macosx-10.9-x86_64'
if 'ARCHFLAGS' not in env :
# https://github.com/python/cpython/blob/a5ed2fe0eedefa1649aa93ee74a0bafc8e628a10/Lib/_osx_support.py#L260
env [ 'ARCHFLAGS' ] = '-arch x86_64'
if 'MACOSX_DEPLOYMENT_TARGET' not in env :
env [ 'MACOSX_DEPLOYMENT_TARGET' ] = '10.9'
2019-11-19 23:36:20 +01:00
2017-04-10 21:42:12 +01:00
# run the before_build command
if before_build :
2018-04-08 18:25:18 +01:00
before_build_prepared = prepare_command ( before_build , project = abs_project_dir )
2017-09-06 22:11:05 +01:00
call ( before_build_prepared , env = env , shell = True )
2017-04-10 21:42:12 +01:00
2017-07-02 18:02:27 -05: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 )
call ([ 'pip' , 'wheel' , abs_project_dir , '-w' , built_wheel_dir , '--no-deps' ] + get_build_verbosity_extra_flags ( build_verbosity ), env = env )
built_wheel = glob ( os . path . join ( built_wheel_dir , '*.whl' ))[ 0 ]
2017-06-01 22:53:52 -05:00
2019-11-12 23:34:59 +00:00
# repair the wheel
if os . path . exists ( repaired_wheel_dir ):
shutil . rmtree ( repaired_wheel_dir )
os . makedirs ( repaired_wheel_dir )
if built_wheel . endswith ( 'none-any.whl' ) or not repair_command :
# pure Python wheel or empty repair command
shutil . move ( built_wheel , repaired_wheel_dir )
2017-04-13 13:48:57 +01:00
else :
2019-11-12 23:34:59 +00:00
repair_command_prepared = prepare_command ( repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir )
call ( repair_command_prepared , env = env , shell = True )
repaired_wheel = glob ( os . path . join ( repaired_wheel_dir , '*.whl' ))[ 0 ]
2017-03-19 21:27:29 +00:00
if test_command :
2019-10-12 10:35:45 +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-02-21 17:25:01 +01:00
call ([ 'pip' , 'install' , 'virtualenv' ], env = env )
2019-10-12 10:35:45 +01:00
venv_dir = tempfile . mkdtemp ()
call ([ 'python' , '-m' , 'virtualenv' , venv_dir ], env = env )
virtualenv_env = env . copy ()
virtualenv_env [ 'PATH' ] = os . pathsep . join ([
os . path . join ( venv_dir , 'bin' ),
virtualenv_env [ 'PATH' ],
])
# Fix some weird issue with the shebang of installed scripts
# See https://github.com/theacodes/nox/issues/44 and https://github.com/pypa/virtualenv/issues/620
virtualenv_env . pop ( '__PYVENV_LAUNCHER__' , None )
# check that we are using the Python from the virtual environment
call ([ 'which' , 'python' ], env = virtualenv_env )
# install the wheel
2019-11-12 23:34:59 +00:00
call ([ 'pip' , 'install' , repaired_wheel + test_extras ], env = virtualenv_env )
2019-10-12 10:35:45 +01:00
# test the wheel
if test_requires :
call ([ 'pip' , 'install' ] + test_requires , env = virtualenv_env )
2019-10-12 10:44:40 +01:00
2017-03-19 21:27:29 +00:00
# run the tests from $HOME, with an absolute path in the command
# (this ensures that Python runs the tests against the installed wheel
# and not the repo code)
2018-04-08 18:25:18 +01:00
test_command_prepared = prepare_command ( test_command , project = abs_project_dir )
2019-10-12 10:44:40 +01:00
call ( test_command_prepared , cwd = os . environ [ 'HOME' ], env = virtualenv_env , shell = True )
2019-10-15 21:40:00 +02:00
2019-10-12 10:35:45 +01:00
# clean up
shutil . rmtree ( venv_dir )
2017-07-02 18:02:27 -05:00
2018-09-08 18:41:39 +01:00
# we're all done here; move it to output (overwrite existing)
2019-11-12 23:34:59 +00:00
dst = os . path . join ( output_dir , os . path . basename ( repaired_wheel ))
shutil . move ( repaired_wheel , dst )