blob: 188660e2eefc32057cecef45273ebd2fdd69933e [file] [log] [blame] [edit]
"""Script to build compiled binary wheels that can be uploaded to PyPI.
The main GitHub workflow where this script is used:
https://github.com/mypyc/mypy_mypyc-wheels/blob/master/.github/workflows/build.yml
This uses cibuildwheel (https://github.com/pypa/cibuildwheel) to build the wheels.
Usage:
build_wheel.py --python-version <python-version> --output-dir <dir>
Wheels for the given Python version will be created in the given directory.
Python version is in form "39".
This works on macOS, Windows and Linux.
You can test locally by using --extra-opts. macOS example:
mypy/misc/build_wheel.py --python-version 39 --output-dir out --extra-opts="--platform macos"
"""
import argparse
import os
import subprocess
from typing import Dict
# Clang package we use on Linux
LLVM_URL = 'https://github.com/mypyc/mypy_mypyc-wheels/releases/download/llvm/llvm-centos-5.tar.gz'
# Mypy repository root
ROOT_DIR = os.path.dirname(os.path.dirname(__file__))
def create_environ(python_version: str) -> Dict[str, str]:
"""Set up environment variables for cibuildwheel."""
env = os.environ.copy()
env['CIBW_BUILD'] = f"cp{python_version}-*"
# Don't build 32-bit wheels
env['CIBW_SKIP'] = "*-manylinux_i686 *-win32"
# Apple Silicon support
# When cross-compiling on Intel, it is not possible to test arm64 and
# the arm64 part of a universal2 wheel. Warnings will be silenced with
# following CIBW_TEST_SKIP
env['CIBW_ARCHS_MACOS'] = "x86_64 arm64"
env['CIBW_TEST_SKIP'] = "*-macosx_arm64"
env['CIBW_BUILD_VERBOSITY'] = '1'
# mypy's isolated builds don't specify the requirements mypyc needs, so install
# requirements and don't use isolated builds. we need to use build-requirements.txt
# with recent mypy commits to get stub packages needed for compilation.
env['CIBW_BEFORE_BUILD'] = """
pip install -r {package}/build-requirements.txt
""".replace('\n', ' ')
# download a copy of clang to use to compile on linux. this was probably built in 2018,
# speeds up compilation 2x
env['CIBW_BEFORE_BUILD_LINUX'] = """
(cd / && curl -L %s | tar xzf -) &&
pip install -r {package}/build-requirements.txt
""".replace('\n', ' ') % LLVM_URL
# the double negative is counterintuitive, https://github.com/pypa/pip/issues/5735
env['CIBW_ENVIRONMENT'] = 'MYPY_USE_MYPYC=1 MYPYC_OPT_LEVEL=3 PIP_NO_BUILD_ISOLATION=no'
env['CIBW_ENVIRONMENT_LINUX'] = (
'MYPY_USE_MYPYC=1 MYPYC_OPT_LEVEL=3 PIP_NO_BUILD_ISOLATION=no ' +
'CC=/opt/llvm/bin/clang'
)
env['CIBW_ENVIRONMENT_WINDOWS'] = (
'MYPY_USE_MYPYC=1 MYPYC_OPT_LEVEL=2 PIP_NO_BUILD_ISOLATION=no'
)
# lxml doesn't have a wheel for Python 3.10 on the manylinux image we use.
# lxml has historically been slow to support new Pythons as well.
env['CIBW_BEFORE_TEST'] = """
(
grep -v lxml {project}/mypy/test-requirements.txt > /tmp/test-requirements.txt
&& cp {project}/mypy/mypy-requirements.txt /tmp/mypy-requirements.txt
&& cp {project}/mypy/build-requirements.txt /tmp/build-requirements.txt
&& pip install -r /tmp/test-requirements.txt
)
""".replace('\n', ' ')
# lxml currently has wheels on Windows and doesn't have grep, so special case
env['CIBW_BEFORE_TEST_WINDOWS'] = "pip install -r {project}/mypy/test-requirements.txt"
# pytest looks for configuration files in the parent directories of where the tests live.
# since we are trying to run the tests from their installed location, we copy those into
# the venv. Ew ew ew.
# We don't run tests that need lxml since we don't install lxml
# We don't run external mypyc tests since there's some issue with compilation on the
# manylinux image we use.
env['CIBW_TEST_COMMAND'] = """
(
DIR=$(python -c 'import mypy, os; dn = os.path.dirname; print(dn(dn(mypy.__path__[0])))')
&& cp '{project}/mypy/pytest.ini' '{project}/mypy/conftest.py' $DIR
&& MYPY_TEST_DIR=$(python -c 'import mypy.test; print(mypy.test.__path__[0])')
&& MYPY_TEST_PREFIX='{project}/mypy' pytest $MYPY_TEST_DIR -k 'not (reports.test or testreports)'
&& MYPYC_TEST_DIR=$(python -c 'import mypyc.test; print(mypyc.test.__path__[0])')
&& MYPY_TEST_PREFIX='{project}/mypy' pytest $MYPYC_TEST_DIR -k 'not test_external'
)
""".replace('\n', ' ')
# i ran into some flaky tests on windows, so only run testcheck. it looks like we
# previously didn't run any tests on windows wheels, so this is a net win.
env['CIBW_TEST_COMMAND_WINDOWS'] = """
bash -c "
(
DIR=$(python -c 'import mypy, os; dn = os.path.dirname; print(dn(dn(mypy.__path__[0])))')
&& TEST_DIR=$(python -c 'import mypy.test; print(mypy.test.__path__[0])')
&& cp '{project}/mypy/pytest.ini' '{project}/mypy/conftest.py' $DIR
&& MYPY_TEST_PREFIX='{project}/mypy' pytest $TEST_DIR/testcheck.py
)
"
""".replace('\n', ' ')
return env
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument('--python-version', required=True, metavar='XY',
help='Python version (e.g. 38 or 39)')
parser.add_argument('--output-dir', required=True, metavar='DIR',
help='Output directory for created wheels')
parser.add_argument('--extra-opts', default='', metavar='OPTIONS',
help='Extra options passed to cibuildwheel verbatim')
args = parser.parse_args()
python_version = args.python_version
output_dir = args.output_dir
extra_opts = args.extra_opts
environ = create_environ(python_version)
script = f'python -m cibuildwheel {extra_opts} --output-dir {output_dir} {ROOT_DIR}'
subprocess.check_call(script, shell=True, env=environ)
if __name__ == '__main__':
main()