2019-11-12 23:51:27 +00:00
import subprocess
import sys
import textwrap
2020-06-17 00:18:40 +02:00
from pathlib import Path , PurePath
2021-01-01 16:53:45 -05:00
from typing import List , NamedTuple , Set
2017-03-19 21:27:29 +00:00
2021-01-22 09:33:22 -05:00
from .architecture import Architecture
2020-06-24 16:43:33 +01:00
from .docker_container import DockerContainer
2020-11-13 16:30:27 +00:00
from .logger import log
2021-06-23 10:47:18 -04:00
from .typing import PathOrStr , assert_never
2021-01-06 13:50:58 -05:00
from .util import (
BuildOptions ,
BuildSelector ,
NonPlatformWheelError ,
get_build_verbosity_extra_flags ,
prepare_command ,
2021-01-09 15:40:40 -05:00
read_python_configs ,
2021-01-06 13:50:58 -05:00
)
2020-02-21 06:42:19 -05:00
2020-04-10 01:53:06 +02:00
class PythonConfiguration ( NamedTuple ):
version : str
identifier : str
2020-06-23 19:46:23 +01:00
path_str : str
@property
2021-01-02 14:32:55 -05:00
def path ( self ) -> PurePath :
2020-06-23 19:46:23 +01:00
return PurePath ( self . path_str )
2020-04-08 00:16:25 +02:00
2020-12-16 23:17:51 +00:00
def get_python_configurations (
2021-01-09 15:40:40 -05:00
build_selector : BuildSelector ,
2021-04-29 20:21:42 -04:00
architectures : Set [ Architecture ],
2020-12-16 23:17:51 +00:00
) -> List [ PythonConfiguration ]:
2021-01-09 15:40:40 -05:00
2021-05-03 11:45:43 -04:00
full_python_configs = read_python_configs ( "linux" )
2021-01-09 15:40:40 -05:00
python_configurations = [ PythonConfiguration ( ** item ) for item in full_python_configs ]
2020-12-12 21:23:27 +00:00
2021-01-01 16:53:45 -05:00
# return all configurations whose arch is in our `architectures` set,
2020-12-28 00:43:24 +00:00
# and match the build/skip rules
2020-12-12 21:23:27 +00:00
return [
2021-04-30 17:56:34 -04:00
c
for c in python_configurations
2020-12-31 16:35:26 +00:00
if any ( c . identifier . endswith ( arch . value ) for arch in architectures )
2020-12-12 21:23:27 +00:00
and build_selector ( c . identifier )
]
2017-04-11 22:57:42 +01:00
2019-04-20 18:52:01 +02:00
2020-04-10 01:44:02 +02:00
def build ( options : BuildOptions ) -> None :
2019-04-20 18:52:01 +02:00
try :
2021-02-14 12:56:33 -05:00
# check docker is installed
2021-05-03 11:45:43 -04:00
subprocess . run ([ "docker" , "--version" ], check = True , stdout = subprocess . DEVNULL )
2019-11-12 23:51:27 +00:00
except Exception :
2021-04-30 17:56:34 -04:00
print (
2021-05-03 11:45:43 -04:00
"cibuildwheel: Docker not found. Docker is required to run Linux builds. "
"If you're building on Travis CI, add `services: [docker]` to your .travis.yml."
"If you're building on Circle CI in Linux, add a `setup_remote_docker` step to your .circleci/config.yml" ,
2021-04-30 17:56:34 -04:00
file = sys . stderr ,
)
2021-01-17 14:13:26 -05:00
sys . exit ( 2 )
2019-04-20 18:52:01 +02:00
2020-04-08 00:16:25 +02:00
assert options . manylinux_images is not None
2020-12-16 23:17:51 +00:00
python_configurations = get_python_configurations ( options . build_selector , options . architectures )
2017-04-11 22:57:42 +01:00
platforms = [
2021-05-03 11:45:43 -04:00
( "cp" , "manylinux_x86_64" , options . manylinux_images [ "x86_64" ]),
( "cp" , "manylinux_i686" , options . manylinux_images [ "i686" ]),
( "cp" , "manylinux_aarch64" , options . manylinux_images [ "aarch64" ]),
( "cp" , "manylinux_ppc64le" , options . manylinux_images [ "ppc64le" ]),
( "cp" , "manylinux_s390x" , options . manylinux_images [ "s390x" ]),
( "pp" , "manylinux_x86_64" , options . manylinux_images [ "pypy_x86_64" ]),
2021-05-24 09:28:18 +02:00
( "pp" , "manylinux_aarch64" , options . manylinux_images [ "pypy_aarch64" ]),
2021-05-24 09:44:50 +02:00
( "pp" , "manylinux_i686" , options . manylinux_images [ "pypy_i686" ]),
2017-04-11 22:57:42 +01:00
]
2020-06-17 00:18:40 +02:00
cwd = Path . cwd ()
2020-06-15 01:53:31 +02:00
abs_package_dir = options . package_dir . resolve ()
2020-06-17 00:18:40 +02:00
if cwd != abs_package_dir and cwd not in abs_package_dir . parents :
2021-05-03 11:45:43 -04:00
raise Exception ( "package_dir must be inside the working directory" )
2020-03-13 13:45:14 +01:00
2021-05-03 11:45:43 -04:00
container_project_path = PurePath ( "/project" )
2020-07-23 20:24:19 +02:00
container_package_dir = container_project_path / abs_package_dir . relative_to ( cwd )
2021-05-03 11:45:43 -04:00
container_output_dir = PurePath ( "/output" )
2020-03-13 13:45:14 +01:00
2019-12-26 17:18:18 +01:00
for implementation , platform_tag , docker_image in platforms :
2021-04-30 17:56:34 -04:00
platform_configs = [
c
for c in python_configurations
if c . identifier . startswith ( implementation ) and c . identifier . endswith ( platform_tag )
]
2017-07-02 17:11:15 -05:00
if not platform_configs :
continue
2017-04-11 22:57:42 +01:00
2018-09-09 16:11:42 +02:00
try :
2021-05-03 11:45:43 -04:00
log . step ( f "Starting Docker image { docker_image } ..." )
2021-04-30 17:56:34 -04:00
with DockerContainer (
docker_image ,
2021-05-03 11:45:43 -04:00
simulate_32_bit = platform_tag . endswith ( "i686" ),
2021-04-30 17:56:34 -04:00
cwd = container_project_path ,
) as docker :
2020-11-01 11:43:28 +00:00
2021-05-03 11:45:43 -04:00
log . step ( "Copying project into Docker..." )
2020-07-23 20:24:19 +02:00
docker . copy_into ( Path . cwd (), container_project_path )
2020-02-02 16:03:33 +00:00
2020-06-23 19:46:23 +01:00
if options . before_all :
2021-05-03 11:45:43 -04:00
log . step ( "Running before_all..." )
2020-03-11 21:55:53 +00:00
2020-06-23 19:46:23 +01:00
env = docker . get_environment ()
2021-05-03 11:45:43 -04:00
env [ "PATH" ] = f '/opt/python/cp38-cp38/bin: { env [ "PATH" ] } '
env [ "PIP_DISABLE_PIP_VERSION_CHECK" ] = "1"
2021-04-30 17:56:34 -04:00
env = options . environment . as_dictionary (
env , executor = docker . environment_executor
)
2020-03-11 21:55:53 +00:00
2021-04-30 17:56:34 -04:00
before_all_prepared = prepare_command (
options . before_all ,
project = container_project_path ,
package = container_package_dir ,
)
2021-05-03 11:45:43 -04:00
docker . call ([ "sh" , "-c" , before_all_prepared ], env = env )
2020-05-15 13:13:13 +02:00
2020-06-23 19:46:23 +01:00
for config in platform_configs :
2020-11-01 11:34:02 +00:00
log . build_start ( config . identifier )
2020-02-15 12:32:33 +00:00
2021-01-02 14:32:55 -05:00
dependency_constraint_flags : List [ PathOrStr ] = []
2020-05-15 13:13:13 +02:00
2020-06-23 19:46:23 +01:00
if options . dependency_constraints :
2021-04-30 17:56:34 -04:00
constraints_file = options . dependency_constraints . get_for_python_version (
config . version
)
2021-05-03 11:45:43 -04:00
container_constraints_file = PurePath ( "/constraints.txt" )
2020-06-14 10:34:44 +01:00
2020-06-23 19:46:23 +01:00
docker . copy_into ( constraints_file , container_constraints_file )
2021-05-03 11:45:43 -04:00
dependency_constraint_flags = [ "-c" , container_constraints_file ]
2020-05-15 13:13:13 +02:00
2021-05-03 11:45:43 -04:00
log . step ( "Setting up build environment..." )
2020-11-01 12:33:14 +00:00
2020-06-23 19:46:23 +01:00
env = docker . get_environment ()
2020-05-15 13:13:13 +02:00
2020-06-23 19:46:23 +01:00
# put this config's python top of the list
2021-05-03 11:45:43 -04:00
python_bin = config . path / "bin"
env [ "PATH" ] = f ' { python_bin } : { env [ "PATH" ] } '
2020-06-23 19:46:23 +01:00
2021-04-30 17:56:34 -04:00
env = options . environment . as_dictionary (
env , executor = docker . environment_executor
)
2020-06-23 19:46:23 +01:00
2021-06-23 10:47:18 -04:00
# check config python is still on PATH
2021-04-30 17:56:34 -04:00
which_python = docker . call (
2021-05-03 11:45:43 -04:00
[ "which" , "python" ], env = env , capture_output = True
2021-04-30 17:56:34 -04:00
) . strip ()
2021-05-03 11:45:43 -04:00
if PurePath ( which_python ) != python_bin / "python" :
2021-04-30 17:56:34 -04: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-06-23 19:46:23 +01:00
2021-05-03 11:45:43 -04:00
which_pip = docker . call ([ "which" , "pip" ], env = env , capture_output = True ) . strip ()
if PurePath ( which_pip ) != python_bin / "pip" :
2021-04-30 17:56:34 -04: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-06-23 19:46:23 +01:00
if options . before_build :
2021-05-03 11:45:43 -04:00
log . step ( "Running before_build..." )
2021-04-30 17:56:34 -04:00
before_build_prepared = prepare_command (
options . before_build ,
project = container_project_path ,
package = container_package_dir ,
)
2021-05-03 11:45:43 -04:00
docker . call ([ "sh" , "-c" , before_build_prepared ], env = env )
2020-06-23 19:46:23 +01:00
2021-05-03 11:45:43 -04:00
log . step ( "Building wheel..." )
2020-11-01 11:34:02 +00:00
2021-05-03 11:45:43 -04:00
temp_dir = PurePath ( "/tmp/cibuildwheel" )
built_wheel_dir = temp_dir / "built_wheel"
docker . call ([ "rm" , "-rf" , built_wheel_dir ])
docker . call ([ "mkdir" , "-p" , built_wheel_dir ])
2020-06-23 19:46:23 +01:00
2021-06-23 10:47:18 -04:00
verbosity_flags = get_build_verbosity_extra_flags ( options . build_verbosity )
if options . build_frontend == "pip" :
docker . call (
[
"python" ,
"-m" ,
"pip" ,
"wheel" ,
container_package_dir ,
f "--wheel-dir= { built_wheel_dir } " ,
"--no-deps" ,
* verbosity_flags ,
],
env = env ,
)
elif options . build_frontend == "build" :
config_setting = " " . join ( verbosity_flags )
docker . call (
[
"python" ,
"-m" ,
"build" ,
container_package_dir ,
"--wheel" ,
f "--outdir= { built_wheel_dir } " ,
f "--config-setting= { config_setting } " ,
],
env = env ,
)
else :
assert_never ( options . build_frontend )
2020-05-11 17:09:54 +02:00
2021-05-03 11:45:43 -04:00
built_wheel = docker . glob ( built_wheel_dir , "*.whl" )[ 0 ]
2020-03-13 12:58:23 +00:00
2021-05-03 11:45:43 -04:00
repaired_wheel_dir = temp_dir / "repaired_wheel"
docker . call ([ "rm" , "-rf" , repaired_wheel_dir ])
docker . call ([ "mkdir" , "-p" , repaired_wheel_dir ])
2020-02-02 17:30:27 +00:00
2021-05-03 11:45:43 -04:00
if built_wheel . name . endswith ( "none-any.whl" ):
2020-07-20 15:35:51 +01:00
raise NonPlatformWheelError ()
if options . repair_command :
2021-05-03 11:45:43 -04:00
log . step ( "Repairing wheel..." )
2021-04-30 17:56:34 -04:00
repair_command_prepared = prepare_command (
options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir
)
2021-05-03 11:45:43 -04:00
docker . call ([ "sh" , "-c" , repair_command_prepared ], env = env )
2020-07-20 15:35:51 +01:00
else :
2021-05-03 11:45:43 -04:00
docker . call ([ "mv" , built_wheel , repaired_wheel_dir ])
2020-03-14 17:21:47 +00:00
2021-05-03 11:45:43 -04:00
repaired_wheels = docker . glob ( repaired_wheel_dir , "*.whl" )
2020-02-02 16:03:33 +00:00
2021-01-20 21:22:48 -05:00
if options . test_command and options . test_selector ( config . identifier ):
2021-05-03 11:45:43 -04:00
log . step ( "Testing wheel..." )
2020-11-01 11:34:02 +00:00
2020-06-23 19:46:23 +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.
2021-04-30 17:56:34 -04:00
docker . call (
2021-05-03 11:45:43 -04:00
[ "pip" , "install" , "virtualenv" , * dependency_constraint_flags ], env = env
2021-04-30 17:56:34 -04:00
)
venv_dir = (
2021-05-03 11:45:43 -04:00
PurePath ( docker . call ([ "mktemp" , "-d" ], capture_output = True ) . strip ())
/ "venv"
2021-04-30 17:56:34 -04:00
)
2020-02-02 16:03:33 +00:00
2021-04-30 17:56:34 -04:00
docker . call (
2021-05-03 11:45:43 -04:00
[ "python" , "-m" , "virtualenv" , "--no-download" , venv_dir ], env = env
2021-04-30 17:56:34 -04:00
)
2020-02-03 20:50:11 +00:00
2020-06-23 19:46:23 +01:00
virtualenv_env = env . copy ()
2021-05-03 11:45:43 -04:00
virtualenv_env [ "PATH" ] = f " { venv_dir / 'bin' } : { virtualenv_env [ 'PATH' ] } "
2020-02-02 16:03:33 +00:00
2020-06-23 19:46:23 +01:00
if options . before_test :
2021-04-30 17:56:34 -04:00
before_test_prepared = prepare_command (
options . before_test ,
project = container_project_path ,
package = container_package_dir ,
)
2021-05-03 11:45:43 -04:00
docker . call ([ "sh" , "-c" , before_test_prepared ], env = virtualenv_env )
2020-02-02 16:03:33 +00:00
2020-06-23 19:46:23 +01:00
# Install the wheel we just built
# Note: If auditwheel produced two wheels, it's because the earlier produced wheel
# conforms to multiple manylinux standards. These multiple versions of the wheel are
# functionally the same, differing only in name, wheel metadata, and possibly include
# different external shared libraries. so it doesn't matter which one we run the tests on.
# Let's just pick the first one.
wheel_to_test = repaired_wheels [ 0 ]
2021-04-30 17:56:34 -04:00
docker . call (
2021-05-03 11:45:43 -04:00
[ "pip" , "install" , str ( wheel_to_test ) + options . test_extras ],
2021-04-30 17:56:34 -04:00
env = virtualenv_env ,
)
2020-02-02 16:03:33 +00:00
2020-06-23 19:46:23 +01:00
# Install any requirements to run the tests
if options . test_requires :
2021-04-30 17:56:34 -04:00
docker . call (
2021-05-03 11:45:43 -04:00
[ "pip" , "install" , * options . test_requires ], env = virtualenv_env
2021-04-30 17:56:34 -04:00
)
2020-02-15 12:32:33 +00:00
2020-06-23 19:46:23 +01:00
# Run the tests from a different directory
2021-04-30 17:56:34 -04:00
test_command_prepared = prepare_command (
options . test_command ,
project = container_project_path ,
package = container_package_dir ,
)
docker . call (
2021-05-03 11:45:43 -04:00
[ "sh" , "-c" , test_command_prepared ], cwd = "/root" , env = virtualenv_env
2021-04-30 17:56:34 -04:00
)
2020-02-15 12:32:33 +00:00
2020-06-23 19:46:23 +01:00
# clean up test environment
2021-05-03 11:45:43 -04:00
docker . call ([ "rm" , "-rf" , venv_dir ])
2020-02-15 12:32:33 +00:00
2020-06-23 19:46:23 +01:00
# move repaired wheels to output
2021-05-03 11:45:43 -04:00
docker . call ([ "mkdir" , "-p" , container_output_dir ])
docker . call ([ "mv" , * repaired_wheels , container_output_dir ])
2020-02-15 12:32:33 +00:00
2020-11-01 11:34:02 +00:00
log . build_end ()
2021-05-03 11:45:43 -04:00
log . step ( "Copying wheels back to host..." )
2020-06-23 19:46:23 +01:00
# copy the output back into the host
docker . copy_out ( container_output_dir , options . output_dir )
2020-11-01 11:43:28 +00:00
log . step_end ()
2020-04-10 14:18:09 +01:00
except subprocess . CalledProcessError as error :
2021-04-30 17:56:34 -04:00
log . step_end_with_error (
2021-05-03 11:45:43 -04:00
f "Command { error . cmd } failed with code { error . returncode } . { error . stdout } "
2021-04-30 17:56:34 -04:00
)
2021-08-26 15:23:57 -07:00
troubleshoot ( options , error )
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-02-02 16:03:33 +00:00
2021-08-26 15:23:57 -07:00
def _matches_prepared_command ( error_cmd : List [ str ], command_template : str ) -> bool :
if len ( error_cmd ) < 3 or error_cmd [ 0 : 2 ] != [ "sh" , "-c" ]:
return False
command_prefix = command_template . split ( "{" , maxsplit = 1 )[ 0 ] . strip ()
return error_cmd [ 2 ] . startswith ( command_prefix )
def troubleshoot ( options : BuildOptions , error : Exception ) -> None :
2020-02-02 16:03:33 +00:00
2021-06-23 10:47:18 -04:00
if isinstance ( error , subprocess . CalledProcessError ) and (
error . cmd [ 0 : 4 ] == [ "python" , "-m" , "pip" , "wheel" ]
or error . cmd [ 0 : 3 ] == [ "python" , "-m" , "build" ]
2021-08-26 15:23:57 -07:00
or _matches_prepared_command ( error . cmd , options . repair_command )
2021-06-23 10:47:18 -04:00
):
# the wheel build step failed
2021-05-03 11:45:43 -04:00
print ( "Checking for common errors..." )
2021-08-26 15:23:57 -07:00
so_files = list ( options . package_dir . glob ( "**/*.so" ))
2020-02-02 14:33:36 +00:00
if so_files :
2021-04-30 17:56:34 -04:00
print (
textwrap . dedent (
2021-05-03 11:45:43 -04:00
"""
2021-05-02 16:37:38 +02:00
NOTE: Shared object (.so) files found in this project.
2020-02-02 14:33:36 +00:00
2021-05-02 16:37:38 +02:00
These files might be built against the wrong OS, causing problems with
2021-08-26 15:23:57 -07:00
auditwheel. If possible, run cibuildwheel in a clean checkout.
2020-02-02 14:33:36 +00:00
2021-05-02 16:37:38 +02:00
If you're using Cython and have previously done an in-place build,
remove those build files (*.so and *.c) before starting cibuildwheel.
2021-08-26 15:23:57 -07:00
setuptools uses the build/ folder to store its build cache. It
may be necessary to remove those build files (*.so and *.o) before
starting cibuildwheel.
Files that belong to a virtual environment are probably not an issue
unless you used a custom command telling cibuildwheel to activate it.
2021-05-03 11:45:43 -04:00
"""
2021-04-30 17:56:34 -04:00
),
file = sys . stderr ,
)
2020-02-02 14:33:36 +00:00
2021-05-03 11:45:43 -04:00
print ( " Files detected:" )
print ( " \n " . join ( f " { f } " for f in so_files ))
print ( "" )