#!/usr/bin/env python3 from __future__ import print_function from contextlib import contextmanager import os import argparse import glob import shutil import tempfile import crayons import twine.commands.upload import setuptools.sandbox REPO_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) @contextmanager def cd(newdir): prevdir = os.getcwd() os.chdir(os.path.expanduser(newdir)) try: yield finally: os.chdir(prevdir) def _generate_dist(dist_type, setup_file, package_name, setup_args): message = 'Generating {dist_type} for {package_name}.'.format( dist_type=dist_type, package_name=package_name, ) print(crayons.white(message, bold=True)) setup_dir = os.path.dirname(setup_file) with cd(setup_dir): setuptools.sandbox.run_setup(setup_file, setup_args) message = '{dist_type} for {package_name} generated under {dir}.\n'.format( dist_type=dist_type, package_name=package_name, dir=setup_dir, ) print(crayons.green(message, bold=True)) def generate_bdist_wheel(setup_file, package_name, universal=False): if universal: _generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel', '--universal']) else: _generate_dist('bdist_wheel', setup_file, package_name, ['bdist_wheel']) def twine_upload(dist_dirs): message = 'Uploading distributions under the following directories:' print(crayons.green(message, bold=True)) for dist_dir in dist_dirs: print(crayons.yellow(dist_dir)) twine.commands.upload.main(dist_dirs) def cleanup(package_dir): build_dir = os.path.join(package_dir, 'build') temp_dir = os.path.join(package_dir, 'temp') dist_dir = os.path.join(package_dir, 'dist') egg_info = os.path.join( package_dir, '{}.egg-info'.format(os.path.basename(package_dir)) ) def _rm_if_it_exists(directory): if os.path.isdir(directory): print(crayons.green('Removing {}/*'.format(directory), bold=True)) shutil.rmtree(directory) _rm_if_it_exists(build_dir) _rm_if_it_exists(temp_dir) _rm_if_it_exists(dist_dir) _rm_if_it_exists(egg_info) def set_variable(fp, variable, value): fh, temp_abs_path = tempfile.mkstemp() with os.fdopen(fh, 'w') as new_file, open(fp) as old_file: for line in old_file: if line.startswith(variable): if isinstance(value, bool): template = '{variable} = {value}\n' else: template = '{variable} = "{value}"\n' new_file.write(template.format(variable=variable, value=value)) else: new_file.write(line) os.remove(fp) shutil.move(temp_abs_path, fp) message = 'Set {variable} in {fp} to {value}.'.format( fp=fp, variable=variable, value=value) print(crayons.white(message, bold=True)) def update_requirements_in_zulip_repo(zulip_repo_dir, version, hash_or_tag): common = os.path.join(zulip_repo_dir, 'requirements', 'common.in') prod = os.path.join(zulip_repo_dir, 'requirements', 'prod.txt') dev = os.path.join(zulip_repo_dir, 'requirements', 'dev.txt') def _edit_reqs_file(reqs, zulip_bots_line, zulip_line): fh, temp_abs_path = tempfile.mkstemp() with os.fdopen(fh, 'w') as new_file, open(reqs) as old_file: for line in old_file: if 'python-zulip-api' in line and 'zulip==' in line: new_file.write(zulip_line) elif 'python-zulip-api' in line and 'zulip_bots' in line: new_file.write(zulip_bots_line) else: new_file.write(line) os.remove(reqs) shutil.move(temp_abs_path, reqs) url_zulip = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}_git&subdirectory={name}\n' url_zulip_bots = 'git+https://github.com/zulip/python-zulip-api.git@{tag}#egg={name}=={version}+git&subdirectory={name}\n' zulip_bots_line = url_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version) zulip_line = url_zulip.format(tag=hash_or_tag, name='zulip', version=version) _edit_reqs_file(prod, zulip_bots_line, zulip_line) _edit_reqs_file(dev, zulip_bots_line, zulip_line) editable_zulip = '-e "{}"\n'.format(url_zulip.rstrip()) editable_zulip_bots = '-e "{}"\n'.format(url_zulip_bots.rstrip()) _edit_reqs_file( common, editable_zulip_bots.format(tag=hash_or_tag, name='zulip_bots', version=version), editable_zulip.format(tag=hash_or_tag, name='zulip', version=version), ) message = 'Updated zulip API package requirements in the main repo.' print(crayons.white(message, bold=True)) def parse_args(): usage = """ Script to automate the PyPA release of the zulip, zulip_bots and zulip_botserver packages. For example, to make a release for version 0.4.0, execute the following steps in order: 1. Run ./tools/provision 2. Activate the virtualenv created by tools/provision 3. To make a release for version 0.4.0, run the following command: ./tools/release-packages --build 0.4.0 --release 4. After Step 3, commit the outstanding changes in your python-zulip-api repo. 5. Create a release tag "0.4.0". 6. Push the commit and the release tag. 7. To update the zulip/requirements/* in the main zulip repo, run (this will update the requirements to install the packages off of the release tag "0.4.0"): ./tools/release-packages update-main-repo PATH_TO_ZULIP_DIR 0.4.0 You can also update the requirements to install the packages from a specific commit, like so: ./tools/release-packages update-main-repo PATH_TO_ZULIP_DIR 0.4.0 --hash abcedef 8. Commit and push the outstanding changes in your zulip/ repo. And you're done! Congrats! """ parser = argparse.ArgumentParser(usage=usage) parser.add_argument('--cleanup', '-c', action='store_true', default=False, help='Remove build directories (dist/, build/, egg-info/, etc).') parser.add_argument('--build', '-b', metavar='VERSION_NUM', help=('Build sdists and wheels for all packages with the' 'specified version number.' ' sdists and wheels are stored in /dist/*.')) parser.add_argument('--release', '-r', action='store_true', default=False, help='Upload the packages to PyPA using twine.') subparsers = parser.add_subparsers(dest='subcommand') parser_main_repo = subparsers.add_parser( 'update-main-repo', help='Update the zulip/requirements/* in the main zulip repo.' ) parser_main_repo.add_argument('repo', metavar='PATH_TO_ZULIP_DIR') parser_main_repo.add_argument('version', metavar='version number of the packages') parser_main_repo.add_argument('--hash', metavar='COMMIT_HASH') return parser.parse_args() def main(): options = parse_args() glob_pattern = os.path.join(REPO_DIR, '*', 'setup.py') setup_py_files = glob.glob(glob_pattern) if options.cleanup: package_dirs = map(os.path.dirname, setup_py_files) for package_dir in package_dirs: cleanup(package_dir) if options.build: package_dirs = map(os.path.dirname, setup_py_files) for package_dir in package_dirs: cleanup(package_dir) zulip_init = os.path.join(REPO_DIR, 'zulip', 'zulip', '__init__.py') set_variable(zulip_init, '__version__', options.build) bots_setup = os.path.join(REPO_DIR, 'zulip_bots', 'setup.py') set_variable(bots_setup, 'ZULIP_BOTS_VERSION', options.build) set_variable(bots_setup, 'IS_PYPA_PACKAGE', True) botserver_setup = os.path.join(REPO_DIR, 'zulip_botserver', 'setup.py') set_variable(botserver_setup, 'ZULIP_BOTSERVER_VERSION', options.build) for setup_file in setup_py_files: package_name = os.path.basename(os.path.dirname(setup_file)) if package_name == 'zulip_bots': generate_bdist_wheel(setup_file, package_name) else: generate_bdist_wheel(setup_file, package_name, universal=True) set_variable(bots_setup, 'IS_PYPA_PACKAGE', False) if options.release: dist_dirs = glob.glob(os.path.join(REPO_DIR, '*', 'dist', '*')) twine_upload(dist_dirs) if options.subcommand == 'update-main-repo': if options.hash: update_requirements_in_zulip_repo(options.repo, options.version, options.hash) else: update_requirements_in_zulip_repo(options.repo, options.version, options.version) if __name__ == '__main__': main()