aboutsummaryrefslogtreecommitdiff
path: root/llvm_tools/patch_manager.py
blob: 801f84691fa307affd2b096b3c5b24eda0400f53 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""A manager for patches."""

import argparse
import enum
import os
from pathlib import Path
import sys
from typing import Callable, Iterable, List, Optional, Tuple

import failure_modes
import get_llvm_hash
import patch_utils
import subprocess_helpers


class GitBisectionCode(enum.IntEnum):
    """Git bisection exit codes.

    Used when patch_manager.py is in the bisection mode,
    as we need to return in what way we should handle
    certain patch failures.
    """

    GOOD = 0
    """All patches applied successfully."""
    BAD = 1
    """The tested patch failed to apply."""
    SKIP = 125


def GetCommandLineArgs(sys_argv: Optional[List[str]]):
    """Get the required arguments from the command line."""

    # Create parser and add optional command-line arguments.
    parser = argparse.ArgumentParser(description="A manager for patches.")

    # Add argument for the LLVM version to use for patch management.
    parser.add_argument(
        "--svn_version",
        type=int,
        help="the LLVM svn version to use for patch management (determines "
        "whether a patch is applicable). Required when not bisecting.",
    )

    # Add argument for the patch metadata file that is in $FILESDIR.
    parser.add_argument(
        "--patch_metadata_file",
        required=True,
        type=Path,
        help='the absolute path to the .json file in "$FILESDIR/" of the '
        "package which has all the patches and their metadata if applicable",
    )

    # Add argument for the absolute path to the unpacked sources.
    parser.add_argument(
        "--src_path",
        required=True,
        type=Path,
        help="the absolute path to the unpacked LLVM sources",
    )

    # Add argument for the mode of the patch manager when handling failing
    # applicable patches.
    parser.add_argument(
        "--failure_mode",
        default=failure_modes.FailureModes.FAIL,
        type=failure_modes.FailureModes,
        help="the mode of the patch manager when handling failed patches "
        "(default: %(default)s)",
    )
    parser.add_argument(
        "--test_patch",
        default="",
        help="The rel_patch_path of the patch we want to bisect the "
        "application of. Not used in other modes.",
    )

    # Add argument for the option to us git am to commit patch or
    # just using patch.
    parser.add_argument(
        "--git_am",
        action="store_true",
        help="If set, use 'git am' to patch instead of GNU 'patch'. ",
    )

    # Parse the command line.
    return parser.parse_args(sys_argv)


def GetHEADSVNVersion(src_path):
    """Gets the SVN version of HEAD in the src tree."""
    git_hash = subprocess_helpers.check_output(
        ["git", "-C", src_path, "rev-parse", "HEAD"]
    )
    return get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip())


def GetCommitHashesForBisection(src_path, good_svn_version, bad_svn_version):
    """Gets the good and bad commit hashes required by `git bisect start`."""

    bad_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, bad_svn_version)

    good_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, good_svn_version)

    return good_commit_hash, bad_commit_hash


def CheckPatchApplies(
    svn_version: int,
    llvm_src_dir: Path,
    patches_json_fp: Path,
    rel_patch_path: str,
) -> GitBisectionCode:
    """Check that a given patch with the rel_patch_path applies in the stack.

    This is used in the bisection mode of the patch manager. It's similiar
    to ApplyAllFromJson, but differs in that the patch with rel_patch_path
    will attempt to apply regardless of its version range, as we're trying
    to identify the SVN version

    Args:
        svn_version: SVN version to test at.
        llvm_src_dir: llvm-project source code diroctory (with a .git).
        patches_json_fp: PATCHES.json filepath.
        rel_patch_path: Relative patch path of the patch we want to check. If
          patches before this patch fail to apply, then the revision is
          skipped.
    """
    with patches_json_fp.open(encoding="utf-8") as f:
        patch_entries = patch_utils.json_to_patch_entries(
            patches_json_fp.parent,
            f,
        )
    with patch_utils.git_clean_context(llvm_src_dir):
        success, _, failed_patches = ApplyPatchAndPrior(
            svn_version,
            llvm_src_dir,
            patch_entries,
            rel_patch_path,
            patch_utils.git_am,
        )
    if success:
        # Everything is good, patch applied successfully.
        print(f"SUCCEEDED applying {rel_patch_path} @ r{svn_version}")
        return GitBisectionCode.GOOD
    if failed_patches and failed_patches[-1].rel_patch_path == rel_patch_path:
        # We attempted to apply this patch, but it failed.
        print(f"FAILED to apply {rel_patch_path} @ r{svn_version}")
        return GitBisectionCode.BAD
    # Didn't attempt to apply the patch, but failed regardless.
    # Skip this revision.
    print(f"SKIPPED {rel_patch_path} @ r{svn_version} due to prior failures")
    return GitBisectionCode.SKIP


def ApplyPatchAndPrior(
    svn_version: int,
    src_dir: Path,
    patch_entries: Iterable[patch_utils.PatchEntry],
    rel_patch_path: str,
    patch_cmd: Optional[Callable] = None,
) -> Tuple[bool, List[patch_utils.PatchEntry], List[patch_utils.PatchEntry]]:
    """Apply a patch, and all patches that apply before it in the patch stack.

    Patches which did not attempt to apply (because their version range didn't
    match and they weren't the patch of interest) do not appear in the output.

    Probably shouldn't be called from outside of CheckPatchApplies, as it
    modifies the source dir contents.

    Returns:
        A tuple where:
            [0]: Did the patch of interest succeed in applying?
            [1]: List of applied patches, potentially containing the patch of
            interest.
            [2]: List of failing patches, potentially containing the patch of
            interest.
    """
    failed_patches: List[patch_utils.PatchEntry] = []
    applied_patches = []
    # We have to apply every patch up to the one we care about,
    # as patches can stack.
    for pe in patch_entries:
        is_patch_of_interest = pe.rel_patch_path == rel_patch_path
        applied, failed_hunks = patch_utils.apply_single_patch_entry(
            svn_version,
            src_dir,
            pe,
            patch_cmd,
            ignore_version_range=is_patch_of_interest,
        )
        meant_to_apply = bool(failed_hunks) or is_patch_of_interest
        if is_patch_of_interest:
            if applied:
                # We applied the patch we wanted to, we can stop.
                applied_patches.append(pe)
                return True, applied_patches, failed_patches
            else:
                # We failed the patch we cared about, we can stop.
                failed_patches.append(pe)
                return False, applied_patches, failed_patches
        else:
            if applied:
                applied_patches.append(pe)
            elif meant_to_apply:
                # Broke before we reached the patch we cared about. Stop.
                failed_patches.append(pe)
                return False, applied_patches, failed_patches
    raise ValueError(f"Did not find patch {rel_patch_path}. " "Does it exist?")


def PrintPatchResults(patch_info: patch_utils.PatchInfo):
    """Prints the results of handling the patches of a package.

    Args:
        patch_info: A dataclass that has information on the patches.
    """

    def _fmt(patches):
        return (str(pe.patch_path()) for pe in patches)

    if patch_info.applied_patches:
        print("\nThe following patches applied successfully:")
        print("\n".join(_fmt(patch_info.applied_patches)))

    if patch_info.failed_patches:
        print("\nThe following patches failed to apply:")
        print("\n".join(_fmt(patch_info.failed_patches)))

    if patch_info.non_applicable_patches:
        print("\nThe following patches were not applicable:")
        print("\n".join(_fmt(patch_info.non_applicable_patches)))

    if patch_info.modified_metadata:
        print(
            "\nThe patch metadata file %s has been modified"
            % os.path.basename(patch_info.modified_metadata)
        )

    if patch_info.disabled_patches:
        print("\nThe following patches were disabled:")
        print("\n".join(_fmt(patch_info.disabled_patches)))

    if patch_info.removed_patches:
        print(
            "\nThe following patches were removed from the patch metadata file:"
        )
        for cur_patch_path in patch_info.removed_patches:
            print("%s" % os.path.basename(cur_patch_path))


def main(sys_argv: List[str]):
    """Applies patches to the source tree and takes action on a failed patch."""

    args_output = GetCommandLineArgs(sys_argv)

    llvm_src_dir = Path(args_output.src_path)
    if not llvm_src_dir.is_dir():
        raise ValueError(f"--src_path arg {llvm_src_dir} is not a directory")
    patches_json_fp = Path(args_output.patch_metadata_file)
    if not patches_json_fp.is_file():
        raise ValueError(
            "--patch_metadata_file arg " f"{patches_json_fp} is not a file"
        )

    def _apply_all(args):
        if args.svn_version is None:
            raise ValueError("--svn_version must be set when applying patches")
        result = patch_utils.apply_all_from_json(
            svn_version=args.svn_version,
            llvm_src_dir=llvm_src_dir,
            patches_json_fp=patches_json_fp,
            patch_cmd=patch_utils.git_am
            if args.git_am
            else patch_utils.gnu_patch,
            continue_on_failure=args.failure_mode
            == failure_modes.FailureModes.CONTINUE,
        )
        PrintPatchResults(result)

    def _disable(args):
        patch_cmd = patch_utils.git_am if args.git_am else patch_utils.gnu_patch
        patch_utils.update_version_ranges(
            args.svn_version, llvm_src_dir, patches_json_fp, patch_cmd
        )

    def _test_single(args):
        if not args.test_patch:
            raise ValueError(
                "Running with bisect_patches requires the " "--test_patch flag."
            )
        svn_version = GetHEADSVNVersion(llvm_src_dir)
        error_code = CheckPatchApplies(
            svn_version,
            llvm_src_dir,
            patches_json_fp,
            args.test_patch,
        )
        # Since this is for bisection, we want to exit with the
        # GitBisectionCode enum.
        sys.exit(int(error_code))

    dispatch_table = {
        failure_modes.FailureModes.FAIL: _apply_all,
        failure_modes.FailureModes.CONTINUE: _apply_all,
        failure_modes.FailureModes.DISABLE_PATCHES: _disable,
        failure_modes.FailureModes.BISECT_PATCHES: _test_single,
    }

    if args_output.failure_mode in dispatch_table:
        dispatch_table[args_output.failure_mode](args_output)


if __name__ == "__main__":
    main(sys.argv[1:])