#!/usr/bin/env python3 import argparse import glob import os import shutil import tempfile from contextlib import contextmanager import crayons import setuptools.sandbox import twine.commands.upload 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, f"{os.path.basename(package_dir)}.egg-info") def _rm_if_it_exists(directory): if os.path.isdir(directory): print(crayons.green(f"Removing {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 = f"Set {variable} in {fp} to {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 = f'-e "{url_zulip.rstrip()}"\n' editable_zulip_bots = f'-e "{url_zulip_bots.rstrip()}"\n' _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)) generate_bdist_wheel(setup_file, package_name) 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()