xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/clean_up_old_llvm_patches.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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