Files
cibuildwheel/cibuildwheel/macos.py
T

344 lines
16 KiB
Python
Raw Normal View History

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
import tempfile
import textwrap
from os import PathLike
2020-06-15 01:53:31 +02:00
from pathlib import Path
from typing import Dict, List, NamedTuple, Optional, Sequence, Union
2020-04-10 01:44:02 +02:00
from .environment import ParsedEnvironment
2020-11-13 16:30:27 +00:00
from .logger import log
from .util import (Architecture, BuildOptions, BuildSelector, NonPlatformWheelError,
2020-11-13 16:30:27 +00:00
download, get_build_verbosity_extra_flags, get_pip_script,
install_certifi_script, prepare_command)
2017-03-19 21:27:29 +00:00
def call(args: Union[str, Sequence[Union[str, PathLike]]], env: Optional[Dict[str, str]] = None, cwd: Optional[str] = None, shell: bool = False) -> int:
# print the command executing for the logs
if shell:
2020-05-09 10:34:59 +02:00
print(f'+ {args}')
else:
print('+ ' + ' '.join(shlex.quote(str(a)) for a in args))
return subprocess.check_call(args, env=env, cwd=cwd, shell=shell)
class PythonConfiguration(NamedTuple):
version: str
identifier: str
url: str
def get_python_configurations(build_selector: BuildSelector) -> List[PythonConfiguration]:
2017-03-19 21:27:29 +00:00
python_configurations = [
# CPython
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'),
2020-12-16 20:05:31 +00:00
# TODO: figure out what's going on in CPython 3.5 on macOS 11. or, remove it :)
# 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'),
2019-11-19 23:36:20 +01:00
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-12-20 10:48:16 -05:00
PythonConfiguration(version='3.7', identifier='cp37-macosx_x86_64', url='https://www.python.org/ftp/python/3.7.9/python-3.7.9-macosx10.9.pkg'),
2020-12-24 10:00:12 -05:00
PythonConfiguration(version='3.8', identifier='cp38-macosx_x86_64', url='https://www.python.org/ftp/python/3.8.7/python-3.8.7-macosx10.9.pkg'),
2020-12-16 21:47:08 -05:00
# TODO: Find some better way to select Universal2 vs. regular (note that this works on 10.9+, regardless of the name)
PythonConfiguration(version='3.9', identifier='cp39-macosx_x86_64', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
PythonConfiguration(version='3.9', identifier='cp39-macosx_universal2', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
PythonConfiguration(version='3.9', identifier='cp39-macosx_arm64', url='https://www.python.org/ftp/python/3.9.1/python-3.9.1-macos11.0.pkg'),
# PyPy
2020-12-16 21:47:08 -05:00
# TODO: may not support 11.0 yet
# PythonConfiguration(version='2.7', identifier='pp27-macosx_x86_64', url='https://downloads.python.org/pypy/pypy2.7-v7.3.3-osx64.tar.bz2'),
2020-12-18 11:32:28 -05:00
# PythonConfiguration(version='3.6', identifier='pp36-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.6-v7.3.3-osx64.tar.bz2'),
# PythonConfiguration(version='3.7', identifier='pp37-macosx_x86_64', url='https://downloads.python.org/pypy/pypy3.7-v7.3.3-osx64.tar.bz2'),
2017-03-19 21:27:29 +00: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-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()
# 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():
shutil.rmtree(SYMLINKS_DIR)
2020-06-15 01:53:31 +02:00
SYMLINKS_DIR.mkdir(parents=True)
(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-06-15 01:53:31 +02:00
def install_cpython(version: str, url: str) -> Path:
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-10-10 18:24:15 +02:00
python_executable = 'python3' if version[0] == '3' else 'python'
installation_bin_path = Path(f'/Library/Frameworks/Python.framework/Versions/{version}/bin')
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'))
# install
call(['sudo', 'installer', '-pkg', '/tmp/Python.pkg', '-target', '/'])
# patch open ssl
if version == '3.5':
open_ssl_patch_url = f'https://github.com/mayeut/patch-macos-python-openssl/releases/download/v1.1.1h/patch-macos-python-{version}-openssl-v1.1.1h.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-10-10 18:24:15 +02:00
call(["sudo", str(installation_bin_path/python_executable), str(install_certifi_script)])
pip_executable = 'pip3' if version[0] == '3' else 'pip'
make_symlinks(installation_bin_path, python_executable, pip_executable)
return installation_bin_path
2020-06-15 01:53:31 +02:00
def install_pypy(version: str, url: str) -> Path:
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)
call(['tar', '-C', '/tmp', '-xf', downloaded_tar_bz2])
2020-12-31 17:50:19 +01:00
# Patch PyPy to make sure headers get installed into a venv
patch_version = '_27' if version == '2.7' else ''
patch_path = Path(__file__).absolute().parent / 'resources' / f'pypy_venv{patch_version}.patch'
call(['patch', '--force', '-d', installation_path, patch_path])
2020-06-15 01:53:31 +02:00
installation_bin_path = installation_path / 'bin'
python_executable = 'pypy3' if version[0] == '3' else 'pypy'
pip_executable = 'pip3' if version[0] == '3' else 'pip'
make_symlinks(installation_bin_path, python_executable, pip_executable)
return installation_bin_path
def setup_python(python_configuration: PythonConfiguration,
dependency_constraint_flags: Sequence[Union[str, PathLike]],
environment: ParsedEnvironment) -> Dict[str, str]:
2020-11-13 16:30:27 +00:00
implementation_id = python_configuration.identifier.split("-")[0]
log.step(f'Installing Python {implementation_id}...')
if implementation_id.startswith('cp'):
2020-04-05 16:03:35 +02:00
installation_bin_path = install_cpython(python_configuration.version, python_configuration.url)
2020-11-13 16:30:27 +00:00
elif implementation_id.startswith('pp'):
2020-04-05 16:03:35 +02:00
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-11-13 16:30:27 +00:00
log.step('Setting up build environment...')
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
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-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':
# 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
if python_configuration.version == '3.9':
if python_configuration.identifier.endswith('x86_64'):
env.setdefault('_PYTHON_HOST_PLATFORM', 'macosx-10.9-x86_64')
env.setdefault('ARCHFLAGS', '-arch x86_64')
elif python_configuration.identifier.endswith('arm64'):
env.setdefault('_PYTHON_HOST_PLATFORM', 'macosx-11.0-arm64')
env.setdefault('ARCHFLAGS', '-arch arm64')
2020-11-13 16:30:27 +00:00
log.step('Installing build tools...')
call(['pip', 'install', '--upgrade', 'setuptools', 'wheel', 'delocate', *dependency_constraint_flags], env=env)
return env
2020-04-05 16:03:35 +02:00
2020-04-10 01:44:02 +02:00
def build(options: BuildOptions) -> None:
if options.architectures != [Architecture.x86_64]:
raise ValueError(textwrap.dedent(f'''
Invalid archs option {options.architectures}. macOS only supports x86_64 for the moment.
If you want to set emulation architectures on Linux, use CIBW_ARCHS_LINUX instead.
'''))
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-11-13 16:30:27 +00:00
try:
if options.before_all:
log.step('Running before_all...')
env = options.environment.as_dictionary(prev_environment=os.environ)
before_all_prepared = prepare_command(options.before_all, project='.', package=options.package_dir)
call([before_all_prepared], shell=True, env=env)
2020-05-11 17:09:54 +02:00
2020-11-13 16:30:27 +00:00
python_configurations = get_python_configurations(options.build_selector)
2018-03-31 13:02:42 +02:00
2020-11-13 16:30:27 +00:00
for config in python_configurations:
log.build_start(config.identifier)
2019-11-17 17:56:07 +01:00
2020-11-13 16:30:27 +00:00
dependency_constraint_flags: Sequence[Union[str, PathLike]] = []
if options.dependency_constraints:
dependency_constraint_flags = [
'-c', options.dependency_constraints.get_for_python_version(config.version)
]
2019-11-19 23:36:20 +01:00
2020-11-13 16:30:27 +00:00
env = setup_python(config, dependency_constraint_flags, options.environment)
2020-11-13 16:30:27 +00:00
if options.before_build:
log.step('Running before_build...')
before_build_prepared = prepare_command(options.before_build, project='.', package=options.package_dir)
call(before_build_prepared, env=env, shell=True)
2020-11-13 16:30:27 +00:00
log.step('Building wheel...')
if built_wheel_dir.exists():
shutil.rmtree(built_wheel_dir)
built_wheel_dir.mkdir(parents=True)
2020-06-25 12:41:21 +01:00
2020-11-13 16:30:27 +00:00
# Path.resolve() is needed. Without it pip wheel may try to fetch package from pypi.org
# see https://github.com/joerick/cibuildwheel/pull/369
call([
'pip', 'wheel',
options.package_dir.resolve(),
'-w', built_wheel_dir,
'--no-deps',
*get_build_verbosity_extra_flags(options.build_verbosity)
], env=env)
2020-11-13 16:30:27 +00:00
built_wheel = next(built_wheel_dir.glob('*.whl'))
2020-11-13 16:30:27 +00:00
if repaired_wheel_dir.exists():
shutil.rmtree(repaired_wheel_dir)
repaired_wheel_dir.mkdir(parents=True)
2020-11-13 16:30:27 +00:00
if built_wheel.name.endswith('none-any.whl'):
raise NonPlatformWheelError()
2020-11-13 16:30:27 +00:00
if options.repair_command:
log.step('Repairing wheel...')
2020-12-31 16:48:43 +00:00
if config.identifier.endswith('universal2'):
delocate_archs = 'x86_64,arm64'
elif config.identifier.endswith('arm64'):
delocate_archs = 'arm64'
else:
delocate_archs = 'x86_64'
repair_command_prepared = prepare_command(
options.repair_command,
wheel=built_wheel,
dest_dir=repaired_wheel_dir,
delocate_archs=delocate_archs,
)
2020-11-13 16:30:27 +00:00
call(repair_command_prepared, env=env, shell=True)
else:
shutil.move(str(built_wheel), repaired_wheel_dir)
2017-03-19 21:27:29 +00:00
2020-11-13 16:30:27 +00:00
repaired_wheel = next(repaired_wheel_dir.glob('*.whl'))
2020-03-01 11:09:22 +00:00
if repaired_wheel.stem.endswith('_universal2'):
# due to a bug in packaging/tags, this universal wheel isn't
# installable on arm64. so we rename it to add a 11_0 platform
# tag.
# See https://github.com/pypa/packaging/pull/380 and
# https://github.com/pypa/packaging/issues/379
renamed_wheel = repaired_wheel.parent / f'{repaired_wheel.stem}.macosx_11_0_universal2.whl'
repaired_wheel.rename(renamed_wheel)
repaired_wheel = renamed_wheel
2020-11-13 16:30:27 +00:00
if options.test_command:
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.
call(['pip', 'install', 'virtualenv', *dependency_constraint_flags], env=env)
venv_dir = Path(tempfile.mkdtemp())
2020-11-13 16:30:27 +00:00
# Use --no-download to ensure determinism by using seed libraries
# built into virtualenv
call(['python', '-m', 'virtualenv', '--no-download', venv_dir], env=env)
2020-11-13 16:30:27 +00:00
virtualenv_env = env.copy()
virtualenv_env['PATH'] = os.pathsep.join([
str(venv_dir / 'bin'),
virtualenv_env['PATH'],
])
2020-11-13 16:30:27 +00:00
# check that we are using the Python from the virtual environment
call(['which', 'python'], env=virtualenv_env)
2020-01-07 00:17:03 +01:00
2020-11-13 16:30:27 +00:00
if options.before_test:
before_test_prepared = prepare_command(options.before_test, project='.', package=options.package_dir)
call(before_test_prepared, env=virtualenv_env, shell=True)
2020-11-13 16:30:27 +00:00
# install the wheel
call(['pip', 'install', str(repaired_wheel) + options.test_extras], env=virtualenv_env)
2019-10-12 10:44:40 +01:00
2020-11-13 16:30:27 +00:00
# test the wheel
if options.test_requires:
call(['pip', 'install'] + options.test_requires, env=virtualenv_env)
2020-11-13 16:30:27 +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)
test_command_prepared = prepare_command(
options.test_command,
project=Path('.').resolve(),
package=options.package_dir.resolve()
)
call(test_command_prepared, cwd=os.environ['HOME'], env=virtualenv_env, shell=True)
2017-07-02 18:02:27 -05:00
2020-11-13 16:30:27 +00:00
# clean up
shutil.rmtree(venv_dir)
# we're all done here; move it to output (overwrite existing)
shutil.move(str(repaired_wheel), options.output_dir)
log.build_end()
except subprocess.CalledProcessError as error:
log.error(f'Command {error.cmd} failed with code {error.returncode}. {error.stdout}')
exit(1)