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