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-09-21 11:34:20 -04:00
from typing import Iterator , 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 (
2021-09-19 00:19:28 -04:00
AllBuildOptions ,
2021-01-06 13:50:58 -05:00
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
2021-09-22 10:51:38 -04:00
class BuildStep ( NamedTuple ):
2021-09-21 11:34:20 -04:00
platform_configs : List [ PythonConfiguration ]
platform_tag : str
docker_image : str
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
2021-09-23 15:27:58 -04:00
def get_build_steps (
2021-09-19 00:19:28 -04:00
all_options : AllBuildOptions , python_configurations : List [ PythonConfiguration ]
2021-09-22 10:51:38 -04:00
) -> Iterator [ BuildStep ]:
2021-09-20 12:17:42 -04:00
platforms = [
( "cp" , "manylinux_x86_64" , "x86_64" ),
( "cp" , "manylinux_i686" , "i686" ),
( "cp" , "manylinux_aarch64" , "aarch64" ),
( "cp" , "manylinux_ppc64le" , "ppc64le" ),
( "cp" , "manylinux_s390x" , "s390x" ),
( "pp" , "manylinux_x86_64" , "pypy_x86_64" ),
( "pp" , "manylinux_aarch64" , "pypy_aarch64" ),
( "pp" , "manylinux_i686" , "pypy_i686" ),
( "cp" , "musllinux_x86_64" , "x86_64" ),
( "cp" , "musllinux_i686" , "i686" ),
( "cp" , "musllinux_aarch64" , "aarch64" ),
( "cp" , "musllinux_ppc64le" , "ppc64le" ),
( "cp" , "musllinux_s390x" , "s390x" ),
]
for implementation , platform_tag , platform_arch in platforms :
platform_configs = [
c
for c in python_configurations
if c . identifier . startswith ( implementation ) and c . identifier . endswith ( platform_tag )
]
if not platform_configs :
continue
2021-09-19 00:19:28 -04:00
for local_configs , docker_image in all_options . produce_image_batches (
platform_configs , platform_tag , platform_arch
):
# TODO: Validate that the options are not invalid for these selectors
yield BuildStep ( local_configs , platform_tag , docker_image )
2021-09-20 12:17:42 -04:00
2021-09-20 12:28:05 -04:00
def build_on_docker (
2021-09-19 00:19:28 -04:00
all_options : AllBuildOptions ,
2021-09-20 12:28:05 -04:00
platform_configs : List [ PythonConfiguration ],
docker : DockerContainer ,
container_project_path : PurePath ,
container_package_dir : PurePath ,
) -> None :
container_output_dir = PurePath ( "/output" )
log . step ( "Copying project into Docker..." )
docker . copy_into ( Path . cwd (), container_project_path )
2021-09-19 00:19:28 -04:00
if all_options . before_all :
2021-09-20 12:28:05 -04:00
log . step ( "Running before_all..." )
env = docker . get_environment ()
env [ "PATH" ] = f '/opt/python/cp38-cp38/bin: { env [ "PATH" ] } '
env [ "PIP_DISABLE_PIP_VERSION_CHECK" ] = "1"
2021-09-19 00:19:28 -04:00
env = all_options . environment . as_dictionary ( env , executor = docker . environment_executor )
2021-09-20 12:28:05 -04:00
before_all_prepared = prepare_command (
2021-09-19 00:19:28 -04:00
all_options . before_all ,
2021-09-20 12:28:05 -04:00
project = container_project_path ,
package = container_package_dir ,
)
docker . call ([ "sh" , "-c" , before_all_prepared ], env = env )
for config in platform_configs :
log . build_start ( config . identifier )
2021-09-19 00:19:28 -04:00
options = all_options [ config . identifier ]
2021-09-20 12:28:05 -04:00
dependency_constraint_flags : List [ PathOrStr ] = []
if options . dependency_constraints :
constraints_file = options . dependency_constraints . get_for_python_version ( config . version )
container_constraints_file = PurePath ( "/constraints.txt" )
docker . copy_into ( constraints_file , container_constraints_file )
dependency_constraint_flags = [ "-c" , container_constraints_file ]
log . step ( "Setting up build environment..." )
env = docker . get_environment ()
# put this config's python top of the list
python_bin = config . path / "bin"
env [ "PATH" ] = f ' { python_bin } : { env [ "PATH" ] } '
env = options . environment . as_dictionary ( env , executor = docker . environment_executor )
# check config python is still on PATH
which_python = docker . call ([ "which" , "python" ], env = env , capture_output = True ) . strip ()
if PurePath ( which_python ) != python_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 ,
)
sys . exit ( 1 )
which_pip = docker . call ([ "which" , "pip" ], env = env , capture_output = True ) . strip ()
if PurePath ( which_pip ) != python_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 ,
)
sys . exit ( 1 )
if options . before_build :
log . step ( "Running before_build..." )
before_build_prepared = prepare_command (
options . before_build ,
project = container_project_path ,
package = container_package_dir ,
)
docker . call ([ "sh" , "-c" , before_build_prepared ], env = env )
log . step ( "Building wheel..." )
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 ])
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 )
built_wheel = docker . glob ( built_wheel_dir , "*.whl" )[ 0 ]
repaired_wheel_dir = temp_dir / "repaired_wheel"
docker . call ([ "rm" , "-rf" , repaired_wheel_dir ])
docker . call ([ "mkdir" , "-p" , repaired_wheel_dir ])
if built_wheel . name . endswith ( "none-any.whl" ):
raise NonPlatformWheelError ()
if options . repair_command :
log . step ( "Repairing wheel..." )
repair_command_prepared = prepare_command (
options . repair_command , wheel = built_wheel , dest_dir = repaired_wheel_dir
)
docker . call ([ "sh" , "-c" , repair_command_prepared ], env = env )
else :
docker . call ([ "mv" , built_wheel , repaired_wheel_dir ])
repaired_wheels = docker . glob ( repaired_wheel_dir , "*.whl" )
if options . test_command and options . test_selector ( config . identifier ):
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.
docker . call ([ "pip" , "install" , "virtualenv" , * dependency_constraint_flags ], env = env )
venv_dir = PurePath ( docker . call ([ "mktemp" , "-d" ], capture_output = True ) . strip ()) / "venv"
docker . call ([ "python" , "-m" , "virtualenv" , "--no-download" , venv_dir ], env = env )
virtualenv_env = env . copy ()
virtualenv_env [ "PATH" ] = f " { venv_dir / 'bin' } : { virtualenv_env [ 'PATH' ] } "
if options . before_test :
before_test_prepared = prepare_command (
options . before_test ,
project = container_project_path ,
package = container_package_dir ,
)
docker . call ([ "sh" , "-c" , before_test_prepared ], env = virtualenv_env )
# 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 ]
docker . call (
[ "pip" , "install" , str ( wheel_to_test ) + options . test_extras ],
env = virtualenv_env ,
)
# Install any requirements to run the tests
if options . test_requires :
docker . call ([ "pip" , "install" , * options . test_requires ], env = virtualenv_env )
# Run the tests from a different directory
test_command_prepared = prepare_command (
options . test_command ,
project = container_project_path ,
package = container_package_dir ,
)
docker . call ([ "sh" , "-c" , test_command_prepared ], cwd = "/root" , env = virtualenv_env )
# clean up test environment
docker . call ([ "rm" , "-rf" , venv_dir ])
# move repaired wheels to output
docker . call ([ "mkdir" , "-p" , container_output_dir ])
docker . call ([ "mv" , * repaired_wheels , container_output_dir ])
log . build_end ()
log . step ( "Copying wheels back to host..." )
# copy the output back into the host
2021-09-19 00:19:28 -04:00
docker . copy_out ( container_output_dir , all_options . output_dir )
2021-09-20 12:28:05 -04:00
log . step_end ()
2021-09-19 00:19:28 -04:00
def build ( all_options : AllBuildOptions ) -> 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
2021-09-19 00:19:28 -04:00
python_configurations = get_python_configurations (
all_options . build_selector , all_options . architectures
)
2017-04-11 22:57:42 +01:00
2020-06-17 00:18:40 +02:00
cwd = Path . cwd ()
2021-09-19 00:19:28 -04:00
abs_package_dir = all_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 )
2020-03-13 13:45:14 +01:00
2021-09-19 00:19:28 -04:00
for build_step in get_build_steps ( all_options , python_configurations ):
2018-09-09 16:11:42 +02:00
try :
2021-09-22 10:51:38 -04:00
log . step ( f "Starting Docker image { build_step . docker_image } ..." )
2021-09-21 11:34:20 -04:00
2021-04-30 17:56:34 -04:00
with DockerContainer (
2021-09-22 10:51:38 -04:00
build_step . docker_image ,
simulate_32_bit = build_step . platform_tag . endswith ( "i686" ),
2021-04-30 17:56:34 -04:00
cwd = container_project_path ,
) as docker :
2021-09-21 11:34:20 -04:00
2021-09-20 12:28:05 -04:00
build_on_docker (
2021-09-19 00:19:28 -04:00
all_options ,
2021-09-22 10:51:38 -04:00
build_step . platform_configs ,
2021-09-21 11:34:20 -04:00
docker ,
container_project_path ,
container_package_dir ,
2021-09-20 12:28:05 -04:00
)
2020-11-01 11:43:28 +00:00
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-09-19 00:19:28 -04:00
troubleshoot ( all_options , error )
2021-01-17 14:13:26 -05:00
sys . exit ( 1 )
2020-02-02 16:03:33 +00:00
2021-08-28 00:44:26 +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 )
2021-09-19 00:19:28 -04:00
def troubleshoot ( all_options : AllBuildOptions , 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-09-19 00:19:28 -04:00
or _matches_prepared_command (
error . cmd , all_options . general_build_options . repair_command
) # TODO
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-09-19 00:19:28 -04:00
so_files = list ( all_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 ( "" )