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 glob import glob
2020-04-08 11:08:23 +02:00
from typing import Callable , Dict , List , Optional , NamedTuple , Union
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 ,
2019-11-12 23:51:27 +00:00
download ,
get_build_verbosity_extra_flags ,
2020-03-01 11:09:22 +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-04-08 00:16:25 +02:00
def call ( args : Union [ str , List [ str ]], env : Optional [ Dict [ str , str ]] = None , cwd : Optional [ str ] = None , shell : bool = False ) -> int :
2020-02-11 14:44:53 +01:00
# 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 )
2020-04-08 11:08:23 +02:00
PythonConfiguration = NamedTuple ( 'PythonConfiguration' , [( 'version' , str ), ( 'identifier' , str ), ( 'url' , str )])
2020-04-08 00:16:25 +02:00
def get_python_configurations ( build_selector : Callable [[ str ], bool ]) -> List [ PythonConfiguration ]:
2017-03-19 21:27:29 +00:00
python_configurations = [
2020-03-18 10:11:09 +00:00
# CPython
2020-04-25 11:16:36 +02:00
PythonConfiguration ( version = '2.7' , identifier = 'cp27-macosx_x86_64' , url = 'https://www.python.org/ftp/python/2.7.18/python-2.7.18-macosx10.9.pkg' ),
2019-11-19 23:36:20 +01:00
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' ),
2020-04-25 13:07:57 +02:00
PythonConfiguration ( version = '3.7' , identifier = 'cp37-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.7.7/python-3.7.7-macosx10.9.pkg' ),
2020-03-07 11:37:11 +01:00
PythonConfiguration ( version = '3.8' , identifier = 'cp38-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.8.2/python-3.8.2-macosx10.9.pkg' ),
2020-03-18 10:11:09 +00:00
# PyPy
2020-04-28 20:05:28 +02:00
PythonConfiguration ( version = '2.7' , identifier = 'pp27-macosx_x86_64' , url = 'https://downloads.python.org/pypy/pypy2.7-v7.3.1-osx64.tar.bz2' ),
PythonConfiguration ( version = '3.6' , identifier = 'pp36-macosx_x86_64' , url = 'https://downloads.python.org/pypy/pypy3.6-v7.3.1-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'
2020-04-08 00:16:25 +02:00
def make_symlinks ( installation_bin_path : str , python_executable : str , pip_executable : str ) -> None :
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-04-08 00:16:25 +02:00
def install_cpython ( version : str , url : str ) -> str :
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' :
2020-04-25 16:14:31 +02:00
open_ssl_patch_url = 'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.0.2u/patch-macos-python- %s -openssl-v1.0.2u.tar.gz' % version
2020-02-11 14:44:53 +01:00
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-04-08 00:16:25 +02:00
def install_pypy ( version : str , url : str ) -> str :
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
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
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-04-05 16:03:35 +02:00
if python_configuration . identifier . startswith ( 'cp' ):
installation_bin_path = install_cpython ( python_configuration . version , python_configuration . url )
elif python_configuration . identifier . startswith ( 'pp' ):
installation_bin_path = install_pypy ( python_configuration . version , python_configuration . url )
else :
raise ValueError ( "Unknown Python implementation" )
2020-04-08 10:24:16 +02:00
2020-04-05 16:03:35 +02:00
env = os . environ . copy ()
env [ 'PATH' ] = os . pathsep . join ([
SYMLINKS_DIR ,
installation_bin_path ,
env [ 'PATH' ],
])
# Fix issue with site.py setting the wrong `sys.prefix`, `sys.exec_prefix`,
# `sys.path`, ... for PyPy: https://foss.heptapod.net/pypy/pypy/issues/3175
# Also fix an issue with the shebang of installed scripts inside the
# testing virtualenv- see https://github.com/theacodes/nox/issues/44 and
# https://github.com/pypa/virtualenv/issues/620
# Also see https://github.com/python/cpython/pull/9516
env . pop ( '__PYVENV_LAUNCHER__' , None )
env = environment . as_dictionary ( prev_environment = env )
# check what version we're on
call ([ 'which' , 'python' ], env = env )
call ([ 'python' , '--version' ], env = env )
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 )
# install pip & wheel
call ([ 'python' , get_pip_script ] + dependency_constraint_flags , env = env , cwd = "/tmp" )
assert os . path . exists ( os . path . join ( installation_bin_path , 'pip' ))
call ([ 'which' , 'pip' ], env = env )
call ([ 'pip' , '--version' ], env = env )
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 )
call ([ 'pip' , 'install' , '--upgrade' , 'setuptools' , 'wheel' , 'delocate' ] + dependency_constraint_flags , env = env )
2020-04-07 18:50:16 +01:00
# Set MACOSX_DEPLOYMENT_TARGET to 10.9, if the user didn't set it.
# CPython 3.5 defaults to 10.6, and pypy defaults to 10.7, causing
# inconsistencies if it's left unset.
env . setdefault ( 'MACOSX_DEPLOYMENT_TARGET' , '10.9' )
2020-04-05 16:03:35 +02:00
if python_configuration . version == '3.5' :
2020-04-07 18:47:22 +01:00
# Cross-compilation platform override - CPython 3.5 has an
# i386/x86_64 version of Python, but we only want a x64_64 build
env . setdefault ( '_PYTHON_HOST_PLATFORM' , 'macosx-10.9-x86_64' )
2020-04-07 18:50:16 +01:00
# https://github.com/python/cpython/blob/a5ed2fe0eedefa1649aa93ee74a0bafc8e628a10/Lib/_osx_support.py#L260
env . setdefault ( 'ARCHFLAGS' , '-arch x86_64' )
2020-04-05 16:03:35 +02:00
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 :
2019-11-12 23:34:59 +00:00
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' )
2020-04-07 00:40:01 +02:00
python_configurations = get_python_configurations ( options . build_selector )
2018-03-31 13:02:42 +02:00
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-17 17:56:07 +01:00
2020-04-07 00:40:01 +02:00
env = setup_python ( config , dependency_constraint_flags , options . environment )
2019-11-19 23:36:20 +01: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-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 )
2020-04-09 16:19:36 +01:00
call ([ '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 ]
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 )
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 )
2017-04-13 13:48:57 +01: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
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
2020-04-07 00:40:01 +02:00
if options . 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-01-28 22:04:37 +00:00
call ([ 'pip' , 'install' , 'virtualenv' ] + dependency_constraint_flags , env = env )
2019-10-12 10:35:45 +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
call ([ 'python' , '-m' , 'virtualenv' , '--no-download' , venv_dir ], env = env )
2019-10-12 10:35:45 +01:00
virtualenv_env = env . copy ()
virtualenv_env [ 'PATH' ] = os . pathsep . join ([
os . path . join ( venv_dir , 'bin' ),
virtualenv_env [ 'PATH' ],
])
2020-03-01 01:50:45 +01:00
virtualenv_env [ "__CIBW_VIRTUALENV_PATH__" ] = venv_dir
2019-10-12 10:35:45 +01:00
# check that we are using the Python from the virtual environment
call ([ '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
call ( before_test_prepared , env = virtualenv_env , shell = True )
2019-10-12 10:35:45 +01:00
# install the wheel
2020-04-07 00:40:01 +02:00
call ([ 'pip' , 'install' , repaired_wheel + options . test_extras ], env = virtualenv_env )
2019-10-12 10:35:45 +01:00
# test the wheel
2020-04-07 00:40:01 +02:00
if options . test_requires :
call ([ 'pip' , 'install' ] + options . 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)
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: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)
2020-04-07 00:40:01 +02:00
dst = os . path . join ( options . output_dir , os . path . basename ( repaired_wheel ))
2019-11-12 23:34:59 +00:00
shutil . move ( repaired_wheel , dst )