1#!/usr/bin/env python3 2# Copyright 2024 The ChromiumOS Authors 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6"""Removes all LLVM patches before a certain point.""" 7 8import argparse 9import importlib.abc 10import importlib.util 11import logging 12from pathlib import Path 13import re 14import subprocess 15import sys 16import textwrap 17from typing import List, Optional 18 19from cros_utils import git_utils 20import patch_utils 21 22 23# The chromiumos-overlay packages to GC patches in. 24PACKAGES_TO_COLLECT = patch_utils.CHROMEOS_PATCHES_JSON_PACKAGES 25 26# Folks who should be on the R-line of any CLs that get uploaded. 27CL_REVIEWERS = (git_utils.REVIEWER_DETECTIVE,) 28 29# Folks who should be on the CC-line of any CLs that get uploaded. 30CL_CC = ("[email protected]",) 31 32 33def maybe_autodetect_cros_overlay(my_dir: Path) -> Optional[Path]: 34 third_party = my_dir.parent.parent 35 cros_overlay = third_party / "chromiumos-overlay" 36 if cros_overlay.exists(): 37 return cros_overlay 38 return None 39 40 41def remove_old_patches(cros_overlay: Path, min_revision: int) -> bool: 42 """Removes patches in cros_overlay. Returns whether changes were made.""" 43 patches_removed = 0 44 for package in PACKAGES_TO_COLLECT: 45 logging.info("GC'ing patches from %s...", package) 46 patches_json = cros_overlay / package / "files/PATCHES.json" 47 removed_patch_files = patch_utils.remove_old_patches( 48 min_revision, patches_json 49 ) 50 if not removed_patch_files: 51 logging.info("No patches removed from %s", patches_json) 52 continue 53 54 patches_removed += len(removed_patch_files) 55 for patch in removed_patch_files: 56 logging.info("Removing %s...", patch) 57 patch.unlink() 58 return patches_removed != 0 59 60 61def commit_changes(cros_overlay: Path, min_rev: int): 62 commit_msg = textwrap.dedent( 63 f""" 64 llvm: remove old patches 65 66 These patches stopped applying before r{min_rev}, so should no longer 67 be needed. 68 69 BUG=b:332601837 70 TEST=CQ 71 """ 72 ) 73 74 subprocess.run( 75 ["git", "commit", "--quiet", "-a", "-m", commit_msg], 76 cwd=cros_overlay, 77 check=True, 78 stdin=subprocess.DEVNULL, 79 ) 80 81 82def upload_changes(cros_overlay: Path, autosubmit_cwd: Path) -> None: 83 cl_ids = git_utils.upload_to_gerrit( 84 cros_overlay, 85 remote="cros", 86 branch="main", 87 reviewers=CL_REVIEWERS, 88 cc=CL_CC, 89 ) 90 91 if len(cl_ids) > 1: 92 raise ValueError(f"Unexpected: wanted just one CL upload; got {cl_ids}") 93 94 cl_id = cl_ids[0] 95 logging.info("Uploaded CL http://crrev.com/c/%s successfully.", cl_id) 96 git_utils.try_set_autosubmit_labels(autosubmit_cwd, cl_id) 97 98 99def find_chromeos_llvm_version(chromiumos_overlay: Path) -> int: 100 sys_devel_llvm = chromiumos_overlay / "sys-devel" / "llvm" 101 102 # Pick this from the name of the stable ebuild; 9999 is a bit harder to 103 # parse, and stable is just as good. 104 stable_llvm_re = re.compile(r"^llvm.*_pre(\d+)-r\d+\.ebuild$") 105 match_gen = ( 106 stable_llvm_re.fullmatch(x.name) for x in sys_devel_llvm.iterdir() 107 ) 108 matches = [int(x.group(1)) for x in match_gen if x] 109 110 if len(matches) != 1: 111 raise ValueError( 112 f"Expected exactly one ebuild name match in {sys_devel_llvm}; " 113 f"found {len(matches)}" 114 ) 115 return matches[0] 116 117 118def find_android_llvm_version(android_toolchain_tree: Path) -> int: 119 android_version_py = ( 120 android_toolchain_tree 121 / "toolchain" 122 / "llvm_android" 123 / "android_version.py" 124 ) 125 126 # Per 127 # https://docs.python.org/3/library/importlib.html#importing-a-source-file-directly. 128 # Parsing this file is undesirable, since `_svn_revision`, as a variable, 129 # isn't meant to be relied on. Let Python handle the logic instead. 130 module_name = "android_version" 131 android_version = sys.modules.get(module_name) 132 if android_version is None: 133 spec = importlib.util.spec_from_file_location( 134 module_name, android_version_py 135 ) 136 if not spec: 137 raise ImportError( 138 f"Failed loading module spec from {android_version_py}" 139 ) 140 android_version = importlib.util.module_from_spec(spec) 141 sys.modules[module_name] = android_version 142 loader = spec.loader 143 if not isinstance(loader, importlib.abc.Loader): 144 raise ValueError( 145 f"Loader for {android_version_py} was of type " 146 f"{type(loader)}; wanted an importlib.util.Loader" 147 ) 148 loader.exec_module(android_version) 149 150 rev = android_version.get_svn_revision() 151 match = re.match(r"r(\d+)", rev) 152 assert match, f"Invalid SVN revision: {rev!r}" 153 return int(match.group(1)) 154 155 156def get_opts(my_dir: Path, argv: List[str]) -> argparse.Namespace: 157 """Returns options for the script.""" 158 159 parser = argparse.ArgumentParser( 160 description=__doc__, 161 formatter_class=argparse.RawDescriptionHelpFormatter, 162 ) 163 parser.add_argument( 164 "--android-toolchain", 165 type=Path, 166 help=""" 167 Path to an android-toolchain repo root. Only meaningful if 168 `--autodetect-revision` is passed. 169 """, 170 ) 171 parser.add_argument( 172 "--gerrit-tool-cwd", 173 type=Path, 174 help=""" 175 Working directory for `gerrit` tool invocations. This should point to 176 somewhere within a ChromeOS source tree. If none is passed, this will 177 try running them in the path specified by `--chromiumos-overlay`. 178 """, 179 ) 180 parser.add_argument( 181 "--chromiumos-overlay", 182 type=Path, 183 help=""" 184 Path to chromiumos-overlay. Will autodetect if none is specified. If 185 autodetection fails and none is specified, this script will fail. 186 """, 187 ) 188 parser.add_argument( 189 "--commit", 190 action="store_true", 191 help="Commit changes after making them.", 192 ) 193 parser.add_argument( 194 "--upload-with-autoreview", 195 action="store_true", 196 help=""" 197 Upload changes after committing them. Implies --commit. Also adds 198 default reviewers, and starts CQ+1 (among other convenience features). 199 """, 200 ) 201 202 revision_opt = parser.add_mutually_exclusive_group(required=True) 203 revision_opt.add_argument( 204 "--revision", 205 type=int, 206 help=""" 207 Revision to delete before (exclusive). All patches that stopped 208 applying before this will be removed. Phrased as an int, e.g., 209 `--revision=1234`. 210 """, 211 ) 212 revision_opt.add_argument( 213 "--autodetect-revision", 214 action="store_true", 215 help=""" 216 Autodetect the value for `--revision`. If this is passed, you must also 217 pass `--android-toolchain`. This sets `--revision` to the _lesser_ of 218 Android's current LLVM version, and ChromeOS'. 219 """, 220 ) 221 opts = parser.parse_args(argv) 222 223 if not opts.chromiumos_overlay: 224 maybe_overlay = maybe_autodetect_cros_overlay(my_dir) 225 if not maybe_overlay: 226 parser.error( 227 "Failed to autodetect --chromiumos-overlay; please pass a value" 228 ) 229 opts.chromiumos_overlay = maybe_overlay 230 231 if not opts.gerrit_tool_cwd: 232 opts.gerrit_tool_cwd = opts.chromiumos_overlay 233 234 if opts.autodetect_revision: 235 if not opts.android_toolchain: 236 parser.error( 237 "--android-toolchain must be passed with --autodetect-revision" 238 ) 239 240 cros_llvm_version = find_chromeos_llvm_version(opts.chromiumos_overlay) 241 logging.info("Detected CrOS LLVM revision: r%d", cros_llvm_version) 242 android_llvm_version = find_android_llvm_version(opts.android_toolchain) 243 logging.info( 244 "Detected Android LLVM revision: r%d", android_llvm_version 245 ) 246 r = min(cros_llvm_version, android_llvm_version) 247 logging.info("Selected minimum LLVM revision: r%d", r) 248 opts.revision = r 249 250 return opts 251 252 253def main(argv: List[str]) -> None: 254 logging.basicConfig( 255 format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " 256 "%(message)s", 257 level=logging.INFO, 258 ) 259 260 my_dir = Path(__file__).resolve().parent 261 opts = get_opts(my_dir, argv) 262 263 cros_overlay = opts.chromiumos_overlay 264 gerrit_tool_cwd = opts.gerrit_tool_cwd 265 upload = opts.upload_with_autoreview 266 commit = opts.commit or upload 267 min_revision = opts.revision 268 269 made_changes = remove_old_patches(cros_overlay, min_revision) 270 if not made_changes: 271 logging.info("No changes made; exiting.") 272 return 273 274 if not commit: 275 logging.info( 276 "Changes were made, but --commit wasn't specified. My job is done." 277 ) 278 return 279 280 logging.info("Committing changes...") 281 commit_changes(cros_overlay, min_revision) 282 if not upload: 283 logging.info("Change with removed patches has been committed locally.") 284 return 285 286 logging.info("Uploading changes...") 287 upload_changes(cros_overlay, gerrit_tool_cwd) 288 logging.info("Change sent for review.") 289 290 291if __name__ == "__main__": 292 main(sys.argv[1:]) 293