diff options
Diffstat (limited to 'llvm_tools/clean_up_old_llvm_patches.py')
-rwxr-xr-x | llvm_tools/clean_up_old_llvm_patches.py | 292 |
1 files changed, 292 insertions, 0 deletions
diff --git a/llvm_tools/clean_up_old_llvm_patches.py b/llvm_tools/clean_up_old_llvm_patches.py new file mode 100755 index 00000000..d1ae54b2 --- /dev/null +++ b/llvm_tools/clean_up_old_llvm_patches.py @@ -0,0 +1,292 @@ +#!/usr/bin/env python3 +# Copyright 2024 The ChromiumOS Authors +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +"""Removes all LLVM patches before a certain point.""" + +import argparse +import importlib.abc +import importlib.util +import logging +from pathlib import Path +import re +import subprocess +import sys +import textwrap +from typing import List, Optional + +from cros_utils import git_utils +import patch_utils + + +# The chromiumos-overlay packages to GC patches in. +PACKAGES_TO_COLLECT = patch_utils.CHROMEOS_PATCHES_JSON_PACKAGES + +# Folks who should be on the R-line of any CLs that get uploaded. +CL_REVIEWERS = (git_utils.REVIEWER_DETECTIVE,) + +# Folks who should be on the CC-line of any CLs that get uploaded. +CL_CC = ("gbiv@google.com",) + + +def maybe_autodetect_cros_overlay(my_dir: Path) -> Optional[Path]: + third_party = my_dir.parent.parent + cros_overlay = third_party / "chromiumos-overlay" + if cros_overlay.exists(): + return cros_overlay + return None + + +def remove_old_patches(cros_overlay: Path, min_revision: int) -> bool: + """Removes patches in cros_overlay. Returns whether changes were made.""" + patches_removed = 0 + for package in PACKAGES_TO_COLLECT: + logging.info("GC'ing patches from %s...", package) + patches_json = cros_overlay / package / "files/PATCHES.json" + removed_patch_files = patch_utils.remove_old_patches( + min_revision, patches_json + ) + if not removed_patch_files: + logging.info("No patches removed from %s", patches_json) + continue + + patches_removed += len(removed_patch_files) + for patch in removed_patch_files: + logging.info("Removing %s...", patch) + patch.unlink() + return patches_removed != 0 + + +def commit_changes(cros_overlay: Path, min_rev: int): + commit_msg = textwrap.dedent( + f""" + llvm: remove old patches + + These patches stopped applying before r{min_rev}, so should no longer + be needed. + + BUG=b:332601837 + TEST=CQ + """ + ) + + subprocess.run( + ["git", "commit", "--quiet", "-a", "-m", commit_msg], + cwd=cros_overlay, + check=True, + stdin=subprocess.DEVNULL, + ) + + +def upload_changes(cros_overlay: Path, autosubmit_cwd: Path) -> None: + cl_ids = git_utils.upload_to_gerrit( + cros_overlay, + remote="cros", + branch="main", + reviewers=CL_REVIEWERS, + cc=CL_CC, + ) + + if len(cl_ids) > 1: + raise ValueError(f"Unexpected: wanted just one CL upload; got {cl_ids}") + + cl_id = cl_ids[0] + logging.info("Uploaded CL http://crrev.com/c/%s successfully.", cl_id) + git_utils.try_set_autosubmit_labels(autosubmit_cwd, cl_id) + + +def find_chromeos_llvm_version(chromiumos_overlay: Path) -> int: + sys_devel_llvm = chromiumos_overlay / "sys-devel" / "llvm" + + # Pick this from the name of the stable ebuild; 9999 is a bit harder to + # parse, and stable is just as good. + stable_llvm_re = re.compile(r"^llvm.*_pre(\d+)-r\d+\.ebuild$") + match_gen = ( + stable_llvm_re.fullmatch(x.name) for x in sys_devel_llvm.iterdir() + ) + matches = [int(x.group(1)) for x in match_gen if x] + + if len(matches) != 1: + raise ValueError( + f"Expected exactly one ebuild name match in {sys_devel_llvm}; " + f"found {len(matches)}" + ) + return matches[0] + + +def find_android_llvm_version(android_toolchain_tree: Path) -> int: + android_version_py = ( + android_toolchain_tree + / "toolchain" + / "llvm_android" + / "android_version.py" + ) + + # Per + # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly. + # Parsing this file is undesirable, since `_svn_revision`, as a variable, + # isn't meant to be relied on. Let Python handle the logic instead. + module_name = "android_version" + android_version = sys.modules.get(module_name) + if android_version is None: + spec = importlib.util.spec_from_file_location( + module_name, android_version_py + ) + if not spec: + raise ImportError( + f"Failed loading module spec from {android_version_py}" + ) + android_version = importlib.util.module_from_spec(spec) + sys.modules[module_name] = android_version + loader = spec.loader + if not isinstance(loader, importlib.abc.Loader): + raise ValueError( + f"Loader for {android_version_py} was of type " + f"{type(loader)}; wanted an importlib.util.Loader" + ) + loader.exec_module(android_version) + + rev = android_version.get_svn_revision() + match = re.match(r"r(\d+)", rev) + assert match, f"Invalid SVN revision: {rev!r}" + return int(match.group(1)) + + +def get_opts(my_dir: Path, argv: List[str]) -> argparse.Namespace: + """Returns options for the script.""" + + parser = argparse.ArgumentParser( + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + parser.add_argument( + "--android-toolchain", + type=Path, + help=""" + Path to an android-toolchain repo root. Only meaningful if + `--autodetect-revision` is passed. + """, + ) + parser.add_argument( + "--gerrit-tool-cwd", + type=Path, + help=""" + Working directory for `gerrit` tool invocations. This should point to + somewhere within a ChromeOS source tree. If none is passed, this will + try running them in the path specified by `--chromiumos-overlay`. + """, + ) + parser.add_argument( + "--chromiumos-overlay", + type=Path, + help=""" + Path to chromiumos-overlay. Will autodetect if none is specified. If + autodetection fails and none is specified, this script will fail. + """, + ) + parser.add_argument( + "--commit", + action="store_true", + help="Commit changes after making them.", + ) + parser.add_argument( + "--upload-with-autoreview", + action="store_true", + help=""" + Upload changes after committing them. Implies --commit. Also adds + default reviewers, and starts CQ+1 (among other convenience features). + """, + ) + + revision_opt = parser.add_mutually_exclusive_group(required=True) + revision_opt.add_argument( + "--revision", + type=int, + help=""" + Revision to delete before (exclusive). All patches that stopped + applying before this will be removed. Phrased as an int, e.g., + `--revision=1234`. + """, + ) + revision_opt.add_argument( + "--autodetect-revision", + action="store_true", + help=""" + Autodetect the value for `--revision`. If this is passed, you must also + pass `--android-toolchain`. This sets `--revision` to the _lesser_ of + Android's current LLVM version, and ChromeOS'. + """, + ) + opts = parser.parse_args(argv) + + if not opts.chromiumos_overlay: + maybe_overlay = maybe_autodetect_cros_overlay(my_dir) + if not maybe_overlay: + parser.error( + "Failed to autodetect --chromiumos-overlay; please pass a value" + ) + opts.chromiumos_overlay = maybe_overlay + + if not opts.gerrit_tool_cwd: + opts.gerrit_tool_cwd = opts.chromiumos_overlay + + if opts.autodetect_revision: + if not opts.android_toolchain: + parser.error( + "--android-toolchain must be passed with --autodetect-revision" + ) + + cros_llvm_version = find_chromeos_llvm_version(opts.chromiumos_overlay) + logging.info("Detected CrOS LLVM revision: r%d", cros_llvm_version) + android_llvm_version = find_android_llvm_version(opts.android_toolchain) + logging.info( + "Detected Android LLVM revision: r%d", android_llvm_version + ) + r = min(cros_llvm_version, android_llvm_version) + logging.info("Selected minimum LLVM revision: r%d", r) + opts.revision = r + + return opts + + +def main(argv: List[str]) -> None: + logging.basicConfig( + format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " + "%(message)s", + level=logging.INFO, + ) + + my_dir = Path(__file__).resolve().parent + opts = get_opts(my_dir, argv) + + cros_overlay = opts.chromiumos_overlay + gerrit_tool_cwd = opts.gerrit_tool_cwd + upload = opts.upload_with_autoreview + commit = opts.commit or upload + min_revision = opts.revision + + made_changes = remove_old_patches(cros_overlay, min_revision) + if not made_changes: + logging.info("No changes made; exiting.") + return + + if not commit: + logging.info( + "Changes were made, but --commit wasn't specified. My job is done." + ) + return + + logging.info("Committing changes...") + commit_changes(cros_overlay, min_revision) + if not upload: + logging.info("Change with removed patches has been committed locally.") + return + + logging.info("Uploading changes...") + upload_changes(cros_overlay, gerrit_tool_cwd) + logging.info("Change sent for review.") + + +if __name__ == "__main__": + main(sys.argv[1:]) |