| #!/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:]) |