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