xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/patch_manager.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
1*760c253cSXin Li#!/usr/bin/env python3
2*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors
3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be
4*760c253cSXin Li# found in the LICENSE file.
5*760c253cSXin Li
6*760c253cSXin Li"""A manager for patches."""
7*760c253cSXin Li
8*760c253cSXin Liimport argparse
9*760c253cSXin Liimport enum
10*760c253cSXin Liimport os
11*760c253cSXin Lifrom pathlib import Path
12*760c253cSXin Liimport sys
13*760c253cSXin Lifrom typing import Callable, Iterable, List, Optional, Tuple
14*760c253cSXin Li
15*760c253cSXin Liimport failure_modes
16*760c253cSXin Liimport get_llvm_hash
17*760c253cSXin Liimport patch_utils
18*760c253cSXin Liimport subprocess_helpers
19*760c253cSXin Li
20*760c253cSXin Li
21*760c253cSXin Liclass GitBisectionCode(enum.IntEnum):
22*760c253cSXin Li    """Git bisection exit codes.
23*760c253cSXin Li
24*760c253cSXin Li    Used when patch_manager.py is in the bisection mode,
25*760c253cSXin Li    as we need to return in what way we should handle
26*760c253cSXin Li    certain patch failures.
27*760c253cSXin Li    """
28*760c253cSXin Li
29*760c253cSXin Li    GOOD = 0
30*760c253cSXin Li    """All patches applied successfully."""
31*760c253cSXin Li    BAD = 1
32*760c253cSXin Li    """The tested patch failed to apply."""
33*760c253cSXin Li    SKIP = 125
34*760c253cSXin Li
35*760c253cSXin Li
36*760c253cSXin Lidef GetCommandLineArgs(sys_argv: Optional[List[str]]):
37*760c253cSXin Li    """Get the required arguments from the command line."""
38*760c253cSXin Li
39*760c253cSXin Li    # Create parser and add optional command-line arguments.
40*760c253cSXin Li    parser = argparse.ArgumentParser(description="A manager for patches.")
41*760c253cSXin Li
42*760c253cSXin Li    # Add argument for the LLVM version to use for patch management.
43*760c253cSXin Li    parser.add_argument(
44*760c253cSXin Li        "--svn_version",
45*760c253cSXin Li        type=int,
46*760c253cSXin Li        help="the LLVM svn version to use for patch management (determines "
47*760c253cSXin Li        "whether a patch is applicable). Required when not bisecting.",
48*760c253cSXin Li    )
49*760c253cSXin Li
50*760c253cSXin Li    # Add argument for the patch metadata file that is in $FILESDIR.
51*760c253cSXin Li    parser.add_argument(
52*760c253cSXin Li        "--patch_metadata_file",
53*760c253cSXin Li        required=True,
54*760c253cSXin Li        type=Path,
55*760c253cSXin Li        help='the absolute path to the .json file in "$FILESDIR/" of the '
56*760c253cSXin Li        "package which has all the patches and their metadata if applicable",
57*760c253cSXin Li    )
58*760c253cSXin Li
59*760c253cSXin Li    # Add argument for the absolute path to the unpacked sources.
60*760c253cSXin Li    parser.add_argument(
61*760c253cSXin Li        "--src_path",
62*760c253cSXin Li        required=True,
63*760c253cSXin Li        type=Path,
64*760c253cSXin Li        help="the absolute path to the unpacked LLVM sources",
65*760c253cSXin Li    )
66*760c253cSXin Li
67*760c253cSXin Li    # Add argument for the mode of the patch manager when handling failing
68*760c253cSXin Li    # applicable patches.
69*760c253cSXin Li    parser.add_argument(
70*760c253cSXin Li        "--failure_mode",
71*760c253cSXin Li        default=failure_modes.FailureModes.FAIL,
72*760c253cSXin Li        type=failure_modes.FailureModes,
73*760c253cSXin Li        help="the mode of the patch manager when handling failed patches "
74*760c253cSXin Li        "(default: %(default)s)",
75*760c253cSXin Li    )
76*760c253cSXin Li    parser.add_argument(
77*760c253cSXin Li        "--test_patch",
78*760c253cSXin Li        default="",
79*760c253cSXin Li        help="The rel_patch_path of the patch we want to bisect the "
80*760c253cSXin Li        "application of. Not used in other modes.",
81*760c253cSXin Li    )
82*760c253cSXin Li
83*760c253cSXin Li    # Add argument for the option to us git am to commit patch or
84*760c253cSXin Li    # just using patch.
85*760c253cSXin Li    parser.add_argument(
86*760c253cSXin Li        "--git_am",
87*760c253cSXin Li        action="store_true",
88*760c253cSXin Li        help="If set, use 'git am' to patch instead of GNU 'patch'. ",
89*760c253cSXin Li    )
90*760c253cSXin Li
91*760c253cSXin Li    # Parse the command line.
92*760c253cSXin Li    return parser.parse_args(sys_argv)
93*760c253cSXin Li
94*760c253cSXin Li
95*760c253cSXin Lidef GetHEADSVNVersion(src_path):
96*760c253cSXin Li    """Gets the SVN version of HEAD in the src tree."""
97*760c253cSXin Li    git_hash = subprocess_helpers.check_output(
98*760c253cSXin Li        ["git", "-C", src_path, "rev-parse", "HEAD"]
99*760c253cSXin Li    )
100*760c253cSXin Li    return get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip())
101*760c253cSXin Li
102*760c253cSXin Li
103*760c253cSXin Lidef GetCommitHashesForBisection(src_path, good_svn_version, bad_svn_version):
104*760c253cSXin Li    """Gets the good and bad commit hashes required by `git bisect start`."""
105*760c253cSXin Li
106*760c253cSXin Li    bad_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, bad_svn_version)
107*760c253cSXin Li
108*760c253cSXin Li    good_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, good_svn_version)
109*760c253cSXin Li
110*760c253cSXin Li    return good_commit_hash, bad_commit_hash
111*760c253cSXin Li
112*760c253cSXin Li
113*760c253cSXin Lidef CheckPatchApplies(
114*760c253cSXin Li    svn_version: int,
115*760c253cSXin Li    llvm_src_dir: Path,
116*760c253cSXin Li    patches_json_fp: Path,
117*760c253cSXin Li    rel_patch_path: str,
118*760c253cSXin Li) -> GitBisectionCode:
119*760c253cSXin Li    """Check that a given patch with the rel_patch_path applies in the stack.
120*760c253cSXin Li
121*760c253cSXin Li    This is used in the bisection mode of the patch manager. It's similiar
122*760c253cSXin Li    to ApplyAllFromJson, but differs in that the patch with rel_patch_path
123*760c253cSXin Li    will attempt to apply regardless of its version range, as we're trying
124*760c253cSXin Li    to identify the SVN version
125*760c253cSXin Li
126*760c253cSXin Li    Args:
127*760c253cSXin Li        svn_version: SVN version to test at.
128*760c253cSXin Li        llvm_src_dir: llvm-project source code diroctory (with a .git).
129*760c253cSXin Li        patches_json_fp: PATCHES.json filepath.
130*760c253cSXin Li        rel_patch_path: Relative patch path of the patch we want to check. If
131*760c253cSXin Li          patches before this patch fail to apply, then the revision is
132*760c253cSXin Li          skipped.
133*760c253cSXin Li    """
134*760c253cSXin Li    with patches_json_fp.open(encoding="utf-8") as f:
135*760c253cSXin Li        patch_entries = patch_utils.json_to_patch_entries(
136*760c253cSXin Li            patches_json_fp.parent,
137*760c253cSXin Li            f,
138*760c253cSXin Li        )
139*760c253cSXin Li    with patch_utils.git_clean_context(llvm_src_dir):
140*760c253cSXin Li        success, _, failed_patches = ApplyPatchAndPrior(
141*760c253cSXin Li            svn_version,
142*760c253cSXin Li            llvm_src_dir,
143*760c253cSXin Li            patch_entries,
144*760c253cSXin Li            rel_patch_path,
145*760c253cSXin Li            patch_utils.git_am,
146*760c253cSXin Li        )
147*760c253cSXin Li    if success:
148*760c253cSXin Li        # Everything is good, patch applied successfully.
149*760c253cSXin Li        print(f"SUCCEEDED applying {rel_patch_path} @ r{svn_version}")
150*760c253cSXin Li        return GitBisectionCode.GOOD
151*760c253cSXin Li    if failed_patches and failed_patches[-1].rel_patch_path == rel_patch_path:
152*760c253cSXin Li        # We attempted to apply this patch, but it failed.
153*760c253cSXin Li        print(f"FAILED to apply {rel_patch_path} @ r{svn_version}")
154*760c253cSXin Li        return GitBisectionCode.BAD
155*760c253cSXin Li    # Didn't attempt to apply the patch, but failed regardless.
156*760c253cSXin Li    # Skip this revision.
157*760c253cSXin Li    print(f"SKIPPED {rel_patch_path} @ r{svn_version} due to prior failures")
158*760c253cSXin Li    return GitBisectionCode.SKIP
159*760c253cSXin Li
160*760c253cSXin Li
161*760c253cSXin Lidef ApplyPatchAndPrior(
162*760c253cSXin Li    svn_version: int,
163*760c253cSXin Li    src_dir: Path,
164*760c253cSXin Li    patch_entries: Iterable[patch_utils.PatchEntry],
165*760c253cSXin Li    rel_patch_path: str,
166*760c253cSXin Li    patch_cmd: Optional[Callable] = None,
167*760c253cSXin Li) -> Tuple[bool, List[patch_utils.PatchEntry], List[patch_utils.PatchEntry]]:
168*760c253cSXin Li    """Apply a patch, and all patches that apply before it in the patch stack.
169*760c253cSXin Li
170*760c253cSXin Li    Patches which did not attempt to apply (because their version range didn't
171*760c253cSXin Li    match and they weren't the patch of interest) do not appear in the output.
172*760c253cSXin Li
173*760c253cSXin Li    Probably shouldn't be called from outside of CheckPatchApplies, as it
174*760c253cSXin Li    modifies the source dir contents.
175*760c253cSXin Li
176*760c253cSXin Li    Returns:
177*760c253cSXin Li        A tuple where:
178*760c253cSXin Li            [0]: Did the patch of interest succeed in applying?
179*760c253cSXin Li            [1]: List of applied patches, potentially containing the patch of
180*760c253cSXin Li            interest.
181*760c253cSXin Li            [2]: List of failing patches, potentially containing the patch of
182*760c253cSXin Li            interest.
183*760c253cSXin Li    """
184*760c253cSXin Li    failed_patches: List[patch_utils.PatchEntry] = []
185*760c253cSXin Li    applied_patches = []
186*760c253cSXin Li    # We have to apply every patch up to the one we care about,
187*760c253cSXin Li    # as patches can stack.
188*760c253cSXin Li    for pe in patch_entries:
189*760c253cSXin Li        is_patch_of_interest = pe.rel_patch_path == rel_patch_path
190*760c253cSXin Li        applied, failed_hunks = patch_utils.apply_single_patch_entry(
191*760c253cSXin Li            svn_version,
192*760c253cSXin Li            src_dir,
193*760c253cSXin Li            pe,
194*760c253cSXin Li            patch_cmd,
195*760c253cSXin Li            ignore_version_range=is_patch_of_interest,
196*760c253cSXin Li        )
197*760c253cSXin Li        meant_to_apply = bool(failed_hunks) or is_patch_of_interest
198*760c253cSXin Li        if is_patch_of_interest:
199*760c253cSXin Li            if applied:
200*760c253cSXin Li                # We applied the patch we wanted to, we can stop.
201*760c253cSXin Li                applied_patches.append(pe)
202*760c253cSXin Li                return True, applied_patches, failed_patches
203*760c253cSXin Li            else:
204*760c253cSXin Li                # We failed the patch we cared about, we can stop.
205*760c253cSXin Li                failed_patches.append(pe)
206*760c253cSXin Li                return False, applied_patches, failed_patches
207*760c253cSXin Li        else:
208*760c253cSXin Li            if applied:
209*760c253cSXin Li                applied_patches.append(pe)
210*760c253cSXin Li            elif meant_to_apply:
211*760c253cSXin Li                # Broke before we reached the patch we cared about. Stop.
212*760c253cSXin Li                failed_patches.append(pe)
213*760c253cSXin Li                return False, applied_patches, failed_patches
214*760c253cSXin Li    raise ValueError(f"Did not find patch {rel_patch_path}. " "Does it exist?")
215*760c253cSXin Li
216*760c253cSXin Li
217*760c253cSXin Lidef PrintPatchResults(patch_info: patch_utils.PatchInfo):
218*760c253cSXin Li    """Prints the results of handling the patches of a package.
219*760c253cSXin Li
220*760c253cSXin Li    Args:
221*760c253cSXin Li        patch_info: A dataclass that has information on the patches.
222*760c253cSXin Li    """
223*760c253cSXin Li
224*760c253cSXin Li    def _fmt(patches):
225*760c253cSXin Li        return (str(pe.patch_path()) for pe in patches)
226*760c253cSXin Li
227*760c253cSXin Li    if patch_info.applied_patches:
228*760c253cSXin Li        print("\nThe following patches applied successfully:")
229*760c253cSXin Li        print("\n".join(_fmt(patch_info.applied_patches)))
230*760c253cSXin Li
231*760c253cSXin Li    if patch_info.failed_patches:
232*760c253cSXin Li        print("\nThe following patches failed to apply:")
233*760c253cSXin Li        print("\n".join(_fmt(patch_info.failed_patches)))
234*760c253cSXin Li
235*760c253cSXin Li    if patch_info.non_applicable_patches:
236*760c253cSXin Li        print("\nThe following patches were not applicable:")
237*760c253cSXin Li        print("\n".join(_fmt(patch_info.non_applicable_patches)))
238*760c253cSXin Li
239*760c253cSXin Li    if patch_info.modified_metadata:
240*760c253cSXin Li        print(
241*760c253cSXin Li            "\nThe patch metadata file %s has been modified"
242*760c253cSXin Li            % os.path.basename(patch_info.modified_metadata)
243*760c253cSXin Li        )
244*760c253cSXin Li
245*760c253cSXin Li    if patch_info.disabled_patches:
246*760c253cSXin Li        print("\nThe following patches were disabled:")
247*760c253cSXin Li        print("\n".join(_fmt(patch_info.disabled_patches)))
248*760c253cSXin Li
249*760c253cSXin Li    if patch_info.removed_patches:
250*760c253cSXin Li        print(
251*760c253cSXin Li            "\nThe following patches were removed from the patch metadata file:"
252*760c253cSXin Li        )
253*760c253cSXin Li        for cur_patch_path in patch_info.removed_patches:
254*760c253cSXin Li            print("%s" % os.path.basename(cur_patch_path))
255*760c253cSXin Li
256*760c253cSXin Li
257*760c253cSXin Lidef main(sys_argv: List[str]):
258*760c253cSXin Li    """Applies patches to the source tree and takes action on a failed patch."""
259*760c253cSXin Li
260*760c253cSXin Li    args_output = GetCommandLineArgs(sys_argv)
261*760c253cSXin Li
262*760c253cSXin Li    llvm_src_dir = Path(args_output.src_path)
263*760c253cSXin Li    if not llvm_src_dir.is_dir():
264*760c253cSXin Li        raise ValueError(f"--src_path arg {llvm_src_dir} is not a directory")
265*760c253cSXin Li    patches_json_fp = Path(args_output.patch_metadata_file)
266*760c253cSXin Li    if not patches_json_fp.is_file():
267*760c253cSXin Li        raise ValueError(
268*760c253cSXin Li            "--patch_metadata_file arg " f"{patches_json_fp} is not a file"
269*760c253cSXin Li        )
270*760c253cSXin Li
271*760c253cSXin Li    def _apply_all(args):
272*760c253cSXin Li        if args.svn_version is None:
273*760c253cSXin Li            raise ValueError("--svn_version must be set when applying patches")
274*760c253cSXin Li        result = patch_utils.apply_all_from_json(
275*760c253cSXin Li            svn_version=args.svn_version,
276*760c253cSXin Li            llvm_src_dir=llvm_src_dir,
277*760c253cSXin Li            patches_json_fp=patches_json_fp,
278*760c253cSXin Li            patch_cmd=patch_utils.git_am
279*760c253cSXin Li            if args.git_am
280*760c253cSXin Li            else patch_utils.gnu_patch,
281*760c253cSXin Li            continue_on_failure=args.failure_mode
282*760c253cSXin Li            == failure_modes.FailureModes.CONTINUE,
283*760c253cSXin Li        )
284*760c253cSXin Li        PrintPatchResults(result)
285*760c253cSXin Li
286*760c253cSXin Li    def _disable(args):
287*760c253cSXin Li        patch_cmd = patch_utils.git_am if args.git_am else patch_utils.gnu_patch
288*760c253cSXin Li        patch_utils.update_version_ranges(
289*760c253cSXin Li            args.svn_version, llvm_src_dir, patches_json_fp, patch_cmd
290*760c253cSXin Li        )
291*760c253cSXin Li
292*760c253cSXin Li    def _test_single(args):
293*760c253cSXin Li        if not args.test_patch:
294*760c253cSXin Li            raise ValueError(
295*760c253cSXin Li                "Running with bisect_patches requires the " "--test_patch flag."
296*760c253cSXin Li            )
297*760c253cSXin Li        svn_version = GetHEADSVNVersion(llvm_src_dir)
298*760c253cSXin Li        error_code = CheckPatchApplies(
299*760c253cSXin Li            svn_version,
300*760c253cSXin Li            llvm_src_dir,
301*760c253cSXin Li            patches_json_fp,
302*760c253cSXin Li            args.test_patch,
303*760c253cSXin Li        )
304*760c253cSXin Li        # Since this is for bisection, we want to exit with the
305*760c253cSXin Li        # GitBisectionCode enum.
306*760c253cSXin Li        sys.exit(int(error_code))
307*760c253cSXin Li
308*760c253cSXin Li    dispatch_table = {
309*760c253cSXin Li        failure_modes.FailureModes.FAIL: _apply_all,
310*760c253cSXin Li        failure_modes.FailureModes.CONTINUE: _apply_all,
311*760c253cSXin Li        failure_modes.FailureModes.DISABLE_PATCHES: _disable,
312*760c253cSXin Li        failure_modes.FailureModes.BISECT_PATCHES: _test_single,
313*760c253cSXin Li    }
314*760c253cSXin Li
315*760c253cSXin Li    if args_output.failure_mode in dispatch_table:
316*760c253cSXin Li        dispatch_table[args_output.failure_mode](args_output)
317*760c253cSXin Li
318*760c253cSXin Li
319*760c253cSXin Liif __name__ == "__main__":
320*760c253cSXin Li    main(sys.argv[1:])
321