blob: ce4f48f7b19189b7ae8b826fbef0cbea103c61ad [file] [log] [blame]
#!/usr/bin/env python3
"""Build and upload mypy packages for Linux and macOS to PyPI.
*** You must first tag the release and use `git push --tags`. ***
Note: This should be run on macOS using official python.org Python 3.6 or
later, as this is the only tested configuration. Use --force to
run anyway.
This uses a fresh repo clone and a fresh virtualenv to avoid depending on
local state.
Ideas for improvements:
- also upload Windows wheels
- try installing the generated packages and running mypy
- try installing the uploaded packages and running mypy
- run tests
- verify that there is a green travis build
"""
import argparse
import getpass
import os
import os.path
import re
import subprocess
import sys
import tempfile
from typing import Any
class Builder:
def __init__(self, version: str, force: bool, no_upload: bool) -> None:
if not re.match(r'0\.[0-9]{3}$', version):
sys.exit('Invalid version {!r} (expected form 0.123)'.format(version))
self.version = version
self.force = force
self.no_upload = no_upload
self.target_dir = tempfile.mkdtemp()
self.repo_dir = os.path.join(self.target_dir, 'mypy')
def build_and_upload(self) -> None:
self.prompt()
self.run_sanity_checks()
print('Temporary target directory: {}'.format(self.target_dir))
self.git_clone_repo()
self.git_check_out_tag()
self.verify_version()
self.make_virtualenv()
self.install_dependencies()
self.make_wheel()
self.make_sdist()
self.download_compiled_wheels()
if not self.no_upload:
self.upload_wheels()
self.upload_sdist()
self.heading('Successfully uploaded wheel and sdist for mypy {}'.format(self.version))
print("<< All done! >>")
else:
self.heading('Successfully built wheel and sdist for mypy {}'.format(self.version))
dist_dir = os.path.join(self.repo_dir, 'dist')
print('Generated packages:')
for fnam in sorted(os.listdir(dist_dir)):
print(' {}'.format(os.path.join(dist_dir, fnam)))
def prompt(self) -> None:
if self.force:
return
extra = '' if self.no_upload else ' and upload'
print('This will build{} PyPI packages for mypy {}.'.format(extra, self.version))
response = input('Proceed? [yN] ')
if response.lower() != 'y':
sys.exit('Exiting')
def verify_version(self) -> None:
version_path = os.path.join(self.repo_dir, 'mypy', 'version.py')
with open(version_path) as f:
contents = f.read()
if "'{}'".format(self.version) not in contents:
sys.stderr.write(
'\nError: Version {} does not match {}/mypy/version.py\n'.format(
self.version, self.repo_dir))
sys.exit(2)
def run_sanity_checks(self) -> None:
if not sys.version_info >= (3, 6):
sys.exit('You must use Python 3.6 or later to build mypy')
if sys.platform != 'darwin' and not self.force:
sys.exit('You should run this on macOS; use --force to go ahead anyway')
os_file = os.path.realpath(os.__file__)
if not os_file.startswith('/Library/Frameworks') and not self.force:
# Be defensive -- Python from brew may produce bad packages, for example.
sys.exit('Error -- run this script using an official Python build from python.org')
if getpass.getuser() == 'root':
sys.exit('This script must not be run as root')
def git_clone_repo(self) -> None:
self.heading('Cloning mypy git repository')
self.run('git clone https://github.com/python/mypy')
def git_check_out_tag(self) -> None:
tag = 'v{}'.format(self.version)
self.heading('Check out {}'.format(tag))
self.run('cd mypy && git checkout {}'.format(tag))
self.run('cd mypy && git submodule update --init')
def make_virtualenv(self) -> None:
self.heading('Creating a fresh virtualenv')
self.run('python3 -m virtualenv -p {} mypy-venv'.format(sys.executable))
def install_dependencies(self) -> None:
self.heading('Installing build dependencies')
self.run_in_virtualenv('pip3 install wheel twine && pip3 install -U setuptools')
def make_wheel(self) -> None:
self.heading('Building wheel')
self.run_in_virtualenv('python3 setup.py bdist_wheel')
def make_sdist(self) -> None:
self.heading('Building sdist')
self.run_in_virtualenv('python3 setup.py sdist')
def download_compiled_wheels(self) -> None:
self.heading('Downloading wheels compiled with mypyc')
self.run_in_virtualenv('misc/download-mypyc-wheels.py %s' % self.version)
def upload_wheels(self) -> None:
self.heading('Uploading wheels')
for name in os.listdir(os.path.join(self.target_dir, 'mypy', 'dist')):
if name.startswith('mypy-{}-'.format(self.version)) and name.endswith('.whl'):
self.run_in_virtualenv(
'twine upload dist/{}'.format(name))
def upload_sdist(self) -> None:
self.heading('Uploading sdist')
self.run_in_virtualenv('twine upload dist/mypy-{}.tar.gz'.format(self.version))
def run(self, cmd: str) -> None:
try:
subprocess.check_call(cmd, shell=True, cwd=self.target_dir)
except subprocess.CalledProcessError:
sys.stderr.write('Error: Command {!r} failed\n'.format(cmd))
sys.exit(1)
def run_in_virtualenv(self, cmd: str) -> None:
self.run('. mypy-venv/bin/activate && cd mypy &&' + cmd)
def heading(self, heading: str) -> None:
print()
print('==== {} ===='.format(heading))
print()
def parse_args() -> Any:
parser = argparse.ArgumentParser(
description='PyPI mypy package uploader (for non-Windows packages only)')
parser.add_argument('--force', action='store_true', default=False,
help='Skip prompts and sanity checks (be careful!)')
parser.add_argument('--no-upload', action='store_true', default=False,
help="Only build packages but don't upload")
parser.add_argument('version', help='Mypy version to release')
return parser.parse_args()
if __name__ == '__main__':
args = parse_args()
builder = Builder(args.version, args.force, args.no_upload)
builder.build_and_upload()