blob: f1f3554ff791ce4253e51b8105ba443d31bfe5f7 [file] [log] [blame] [edit]
#!/usr/bin/env python3
#
# Copyright (C) 2023 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import argparse
import os
import re
import subprocess
import sys
import shutil
from typing import Optional
# Color codes
RED = '\033[0;31m'
GREEN = '\033[0;32m'
YELLOW = '\033[0;33m'
NC = '\033[0m' # No Color
# Constants for paths. Assumes the script is run from the repository root.
SETUP_PY_PATH = 'python/setup.py'
VENV_PYTHON = os.path.abspath('.venv/bin/python')
def info(msg: str) -> None:
print(f"{GREEN}INFO:{NC} {msg}")
def warn(msg: str) -> None:
print(f"{YELLOW}WARN:{NC} {msg}")
def error(msg: str) -> None:
print(f"{RED}ERROR:{NC} {msg}", file=sys.stderr)
sys.exit(1)
def prompt(msg: str) -> str:
return input(f"{YELLOW}ACTION:{NC} {msg}")
def confirm(msg: str) -> None:
reply = input(f"{msg} [y/N] ").lower().strip()
if reply not in ['y', 'yes']:
error("Aborted by user.")
def run_cmd(*args: str, check: bool = True, cwd: Optional[str] = None) -> None:
info(f"Running command: {' '.join(args)}")
subprocess.run(args, check=check, cwd=cwd)
def check_git_clean() -> None:
"""Checks if the git working directory is clean."""
info("Checking for clean git working directory...")
try:
subprocess.check_call(['git', 'diff', '--quiet'])
subprocess.check_call(['git', 'diff', '--cached', '--quiet'])
except subprocess.CalledProcessError:
error("Git working directory is not clean. Please commit or stash changes.")
info("Git working directory is clean.")
def get_current_branch() -> str:
"""Returns the current git branch."""
return subprocess.check_output(['git', 'rev-parse', '--abbrev-ref',
'HEAD']).decode('utf-8').strip()
def get_setup_py_content() -> str:
with open(SETUP_PY_PATH, 'r') as f:
return f.read()
def write_setup_py_content(content: str) -> None:
with open(SETUP_PY_PATH, 'w') as f:
f.write(content)
def get_current_version(content: str) -> str:
"""Extracts version from setup.py using a robust regex."""
match = re.search(r"version\s*=\s*'([^']*)'", content)
if not match:
error(f"Could not find version in {SETUP_PY_PATH}")
sys.exit(1) # Unreachable, but satisfies type checker
return match.group(1)
def bump_version() -> None:
"""Stage 1: Creates a commit with a bumped version number."""
info("--- Stage 1: Bumping version ---")
content = get_setup_py_content()
current_version = get_current_version(content)
info(f"Current version is {current_version}")
new_version = prompt("Enter the new version (e.g., X.Y.Z): ")
if not re.match(r'\d+\.\d+\.\d+', new_version):
error("Invalid version format. Please use 'X.Y.Z'.")
branch_name = prompt("Enter a name for the new release branch: ")
run_cmd('git', 'checkout', '-b', branch_name)
info(f"Updating version in {SETUP_PY_PATH} to {new_version}...")
new_content = re.sub(r"version\s*=\s*'[^']*'", f"version='{new_version}'",
content)
write_setup_py_content(new_content)
run_cmd('git', 'add', SETUP_PY_PATH)
run_cmd('git', 'commit', '-m',
f'perfetto(python): Bump version to {new_version}')
info(f"Version bump commit created on branch '{branch_name}'.")
info("Please push this branch, create a pull request, and wait for it to "
"be landed.")
def publish(commit: str) -> None:
"""Stage 2: Publishes the package and creates a commit to update the URL."""
info(f"--- Stage 2: Publishing release for commit {commit} ---")
original_branch = get_current_branch()
info(f"Checking out commit {commit}...")
run_cmd('git', 'checkout', commit)
# Read the original content of setup.py at the release commit.
content_at_commit = get_setup_py_content()
new_version = get_current_version(content_at_commit)
download_url = f"'https://github.com/google/perfetto/archive/{commit}.zip'"
# Temporarily update download_url just for building the package.
info(f"Temporarily setting download_url to {download_url}")
temp_content = re.sub(r"download_url\s*=\s*'[^']*'",
f"download_url={download_url}", content_at_commit)
write_setup_py_content(temp_content)
try:
info("Installing build dependencies into the virtual environment...")
run_cmd(VENV_PYTHON, '-m', 'pip', 'install', 'build', 'twine')
info("Building python package...")
run_cmd(VENV_PYTHON, '-m', 'build', cwd='python')
confirm("Ready to upload to PyPI. This is not reversible. Continue?")
run_cmd(
VENV_PYTHON,
'-m',
'twine',
'upload',
'dist/*',
'--verbose',
cwd='python')
info("Successfully published to PyPI.")
finally:
# Always clean up and restore the repository to its original state.
info("Cleaning up build artifacts...")
shutil.rmtree('python/dist', ignore_errors=True)
shutil.rmtree('python/perfetto.egg-info', ignore_errors=True)
info(f"Restoring {SETUP_PY_PATH} to its state at commit {commit}...")
write_setup_py_content(content_at_commit)
info(f"Returning to original branch '{original_branch}'...")
run_cmd('git', 'checkout', original_branch)
# After successful publishing, create the final commit with the download_url.
info("--- Creating final commit to update download_url ---")
final_branch_name = prompt(
"Enter a name for the final download_url update branch: ")
run_cmd('git', 'checkout', '-b', final_branch_name)
info(f"Updating download_url permanently in {SETUP_PY_PATH}...")
final_content = get_setup_py_content()
final_content = re.sub(r"download_url\s*=\s*'[^']*'",
f"download_url={download_url}", final_content)
write_setup_py_content(final_content)
run_cmd('git', 'add', SETUP_PY_PATH)
run_cmd('git', 'commit', '-m',
f'perfetto(python): Update download_url for v{new_version}')
info(f"Commit for download_url created on branch '{final_branch_name}'.")
info("Please push this branch and create a pull request to complete the "
"release.")
def main() -> None:
parser = argparse.ArgumentParser(
description="Automates the Perfetto Python library release process.")
parser.add_argument(
'--bump-version',
action='store_true',
help="Stage 1: Bump version and create a release CL.")
parser.add_argument(
'--publish',
action='store_true',
help="Stage 2: Publish the release to PyPI.")
parser.add_argument(
'--commit',
metavar='HASH',
help="The landed commit hash of the version bump CL (for --publish).")
args = parser.parse_args()
# Ensure script is run from the repository root.
if not os.path.exists(SETUP_PY_PATH):
error(f"This script must be run from the root of the "
f"perfetto repository.")
# Ensure the virtual environment exists.
if not os.path.exists(VENV_PYTHON):
error(f"This script requires a virtual environment at '{VENV_PYTHON}'.")
check_git_clean()
if args.bump_version:
if args.publish or args.commit:
parser.error("--bump-version cannot be used with --publish or --commit.")
bump_version()
elif args.publish:
if not args.commit:
parser.error("--publish requires --commit.")
publish(args.commit)
else:
parser.print_help()
if __name__ == '__main__':
main()