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
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
2020-04-08 00:16:25 +02:00
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2020-06-23 20:28:54 +01:00
from .util import ( BuildOptions , BuildSelector , download ,
get_build_verbosity_extra_flags , get_pip_script ,
prepare_command )
2017-03-19 21:27:29 +00:00
2017-04-10 21:42:12 +01:00
2020-06-23 20:28:54 +01:00
def call ( args : Union [ str , Sequence [ Union [ str , PathLike ]]], 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 :
2020-05-09 10:34:59 +02:00
print ( f '+ { args } ' )
2020-02-11 14:44:53 +01:00
else :
2020-06-23 20:28:54 +01:00
print ( '+ ' + ' ' . join ( shlex . quote ( str ( a )) for a in args ))
2020-02-11 14:44:53 +01:00
return subprocess . check_call ( args , env = env , cwd = cwd , shell = shell )
2020-04-10 01:53:06 +02:00
class PythonConfiguration ( NamedTuple ):
version : str
identifier : str
url : 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 ]:
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-07-04 18:11:06 +02:00
PythonConfiguration ( version = '3.7' , identifier = 'cp37-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.7.8/python-3.7.8-macosx10.9.pkg' ),
2020-05-15 13:35:05 +02:00
PythonConfiguration ( version = '3.8' , identifier = 'cp38-macosx_x86_64' , url = 'https://www.python.org/ftp/python/3.8.3/python-3.8.3-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-06-15 01:53:31 +02:00
SYMLINKS_DIR = Path ( '/tmp/cibw_bin' )
2020-02-19 17:09:18 +01:00
2020-06-15 01:53:31 +02:00
def make_symlinks ( installation_bin_path : Path , python_executable : str , pip_executable : str ) -> None :
assert ( installation_bin_path / python_executable ) . exists ()
2020-02-16 14:32:19 +01:00
# 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-06-15 01:53:31 +02:00
if SYMLINKS_DIR . exists ():
2020-02-19 17:09:18 +01:00
shutil . rmtree ( SYMLINKS_DIR )
2020-06-15 01:53:31 +02:00
SYMLINKS_DIR . mkdir ( parents = True )
2020-02-16 14:32:19 +01:00
2020-06-21 13:14:53 +02:00
( SYMLINKS_DIR / 'python' ) . symlink_to ( installation_bin_path / python_executable )
( SYMLINKS_DIR / 'python-config' ) . symlink_to ( installation_bin_path / ( python_executable + '-config' ))
( SYMLINKS_DIR / 'pip' ) . symlink_to ( installation_bin_path / pip_executable )
2020-02-16 14:32:19 +01:00
2020-06-15 01:53:31 +02:00
def install_cpython ( version : str , url : str ) -> Path :
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
2020-05-09 10:34:59 +02:00
python_package_identifier = f 'org.python.Python.PythonFramework- { version } '
2020-02-11 14:44:53 +01:00
if python_package_identifier not in installed_system_packages :
# download the pkg
2020-06-15 01:53:31 +02:00
download ( url , Path ( '/tmp/Python.pkg' ))
2020-02-11 14:44:53 +01:00
# install
call ([ 'sudo' , 'installer' , '-pkg' , '/tmp/Python.pkg' , '-target' , '/' ])
# patch open ssl
if version == '3.5' :
2020-05-09 10:34:59 +02:00
open_ssl_patch_url = f 'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.0.2u/patch-macos-python- { version } -openssl-v1.0.2u.tar.gz'
2020-06-15 01:53:31 +02:00
download ( open_ssl_patch_url , Path ( '/tmp/python-patch.tar.gz' ))
2020-05-09 10:34:59 +02:00
call ([ 'sudo' , 'tar' , '-C' , f '/Library/Frameworks/Python.framework/Versions/ { version } /' , '-xmf' , '/tmp/python-patch.tar.gz' ])
2020-02-11 14:44:53 +01:00
2020-06-15 01:53:31 +02:00
installation_bin_path = Path ( f '/Library/Frameworks/Python.framework/Versions/ { version } /bin' )
2020-02-16 14:32:19 +01:00
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-06-15 01:53:31 +02:00
def install_pypy ( version : str , url : str ) -> Path :
2020-02-11 14:44:53 +01:00
pypy_tar_bz2 = url . rsplit ( '/' , 1 )[ - 1 ]
2020-06-15 01:53:31 +02:00
extension = ".tar.bz2"
assert pypy_tar_bz2 . endswith ( extension )
pypy_base_filename = pypy_tar_bz2 [: - len ( extension )]
installation_path = Path ( '/tmp' ) / pypy_base_filename
if not installation_path . exists ():
downloaded_tar_bz2 = Path ( "/tmp" ) / pypy_tar_bz2
download ( url , downloaded_tar_bz2 )
2020-06-23 20:28:54 +01:00
call ([ 'tar' , '-C' , '/tmp' , '-xf' , downloaded_tar_bz2 ])
2020-02-16 14:32:19 +01:00
2020-06-15 01:53:31 +02:00
installation_bin_path = installation_path / 'bin'
2020-02-16 14:32:19 +01:00
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-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-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 ([
2020-06-15 01:53:31 +02:00
str ( SYMLINKS_DIR ),
str ( installation_bin_path ),
2020-04-05 16:03:35 +02:00
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
2020-06-23 20:28:54 +01:00
call ([ 'python' , get_pip_script , * dependency_constraint_flags ], env = env , cwd = "/tmp" )
2020-06-15 01:53:31 +02:00
assert ( installation_bin_path / 'pip' ) . exists ()
2020-04-05 16:03:35 +02:00
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 )
2020-06-23 20:28:54 +01:00
call ([ 'pip' , 'install' , '--upgrade' , 'setuptools' , 'wheel' , 'delocate' , * dependency_constraint_flags ], env = env )
2020-04-05 16:03:35 +02:00
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 :
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'
2019-11-12 23:34:59 +00: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-05-15 13:13:13 +02:00
call ([ before_all_prepared ], shell = True , 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 )
2018-03-31 13:02:42 +02:00
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-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
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 )
2020-06-23 20:28:54 +01:00
2020-06-15 01:53:31 +02:00
# 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-25 12:41:21 +01:00
call ([
'pip' , 'wheel' ,
options . package_dir . resolve (),
'-w' , built_wheel_dir ,
'--no-deps' ,
* get_build_verbosity_extra_flags ( options . build_verbosity )
], env = env )
2020-06-15 01:53:31 +02:00
built_wheel = next ( built_wheel_dir . glob ( '*.whl' ))
2017-06-01 22:53:52 -05:00
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-24 23:04:15 +00:00
shutil . move ( str ( 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 )
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: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-06-23 20:28:54 +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-06-23 20:28:54 +01: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 ([
2020-06-15 01:53:31 +02:00
str ( venv_dir / 'bin' ),
2019-10-12 10:35:45 +01:00
virtualenv_env [ 'PATH' ],
])
# 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-06-15 01:53:31 +02:00
call ([ 'pip' , 'install' , str ( 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 ,
2020-06-15 01:53:31 +02:00
project = Path ( '.' ) . resolve (),
2020-06-16 18:56:25 +02:00
package = options . package_dir . resolve ()
2020-04-09 16:19:36 +01:00
)
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-06-24 23:04:15 +00:00
shutil . move ( str ( repaired_wheel ), options . output_dir )