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 Callable, Iterable, List, Optional, Tuple 14 15import failure_modes 16import get_llvm_hash 17import patch_utils 18import subprocess_helpers 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=failure_modes.FailureModes.FAIL, 72 type=failure_modes.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 # Add argument for the option to us git am to commit patch or 84 # just using patch. 85 parser.add_argument( 86 "--git_am", 87 action="store_true", 88 help="If set, use 'git am' to patch instead of GNU 'patch'. ", 89 ) 90 91 # Parse the command line. 92 return parser.parse_args(sys_argv) 93 94 95def GetHEADSVNVersion(src_path): 96 """Gets the SVN version of HEAD in the src tree.""" 97 git_hash = subprocess_helpers.check_output( 98 ["git", "-C", src_path, "rev-parse", "HEAD"] 99 ) 100 return get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip()) 101 102 103def GetCommitHashesForBisection(src_path, good_svn_version, bad_svn_version): 104 """Gets the good and bad commit hashes required by `git bisect start`.""" 105 106 bad_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, bad_svn_version) 107 108 good_commit_hash = get_llvm_hash.GetGitHashFrom(src_path, good_svn_version) 109 110 return good_commit_hash, bad_commit_hash 111 112 113def CheckPatchApplies( 114 svn_version: int, 115 llvm_src_dir: Path, 116 patches_json_fp: Path, 117 rel_patch_path: str, 118) -> GitBisectionCode: 119 """Check that a given patch with the rel_patch_path applies in the stack. 120 121 This is used in the bisection mode of the patch manager. It's similiar 122 to ApplyAllFromJson, but differs in that the patch with rel_patch_path 123 will attempt to apply regardless of its version range, as we're trying 124 to identify the SVN version 125 126 Args: 127 svn_version: SVN version to test at. 128 llvm_src_dir: llvm-project source code diroctory (with a .git). 129 patches_json_fp: PATCHES.json filepath. 130 rel_patch_path: Relative patch path of the patch we want to check. If 131 patches before this patch fail to apply, then the revision is 132 skipped. 133 """ 134 with patches_json_fp.open(encoding="utf-8") as f: 135 patch_entries = patch_utils.json_to_patch_entries( 136 patches_json_fp.parent, 137 f, 138 ) 139 with patch_utils.git_clean_context(llvm_src_dir): 140 success, _, failed_patches = ApplyPatchAndPrior( 141 svn_version, 142 llvm_src_dir, 143 patch_entries, 144 rel_patch_path, 145 patch_utils.git_am, 146 ) 147 if success: 148 # Everything is good, patch applied successfully. 149 print(f"SUCCEEDED applying {rel_patch_path} @ r{svn_version}") 150 return GitBisectionCode.GOOD 151 if failed_patches and failed_patches[-1].rel_patch_path == rel_patch_path: 152 # We attempted to apply this patch, but it failed. 153 print(f"FAILED to apply {rel_patch_path} @ r{svn_version}") 154 return GitBisectionCode.BAD 155 # Didn't attempt to apply the patch, but failed regardless. 156 # Skip this revision. 157 print(f"SKIPPED {rel_patch_path} @ r{svn_version} due to prior failures") 158 return GitBisectionCode.SKIP 159 160 161def ApplyPatchAndPrior( 162 svn_version: int, 163 src_dir: Path, 164 patch_entries: Iterable[patch_utils.PatchEntry], 165 rel_patch_path: str, 166 patch_cmd: Optional[Callable] = None, 167) -> Tuple[bool, List[patch_utils.PatchEntry], List[patch_utils.PatchEntry]]: 168 """Apply a patch, and all patches that apply before it in the patch stack. 169 170 Patches which did not attempt to apply (because their version range didn't 171 match and they weren't the patch of interest) do not appear in the output. 172 173 Probably shouldn't be called from outside of CheckPatchApplies, as it 174 modifies the source dir contents. 175 176 Returns: 177 A tuple where: 178 [0]: Did the patch of interest succeed in applying? 179 [1]: List of applied patches, potentially containing the patch of 180 interest. 181 [2]: List of failing patches, potentially containing the patch of 182 interest. 183 """ 184 failed_patches: List[patch_utils.PatchEntry] = [] 185 applied_patches = [] 186 # We have to apply every patch up to the one we care about, 187 # as patches can stack. 188 for pe in patch_entries: 189 is_patch_of_interest = pe.rel_patch_path == rel_patch_path 190 applied, failed_hunks = patch_utils.apply_single_patch_entry( 191 svn_version, 192 src_dir, 193 pe, 194 patch_cmd, 195 ignore_version_range=is_patch_of_interest, 196 ) 197 meant_to_apply = bool(failed_hunks) or is_patch_of_interest 198 if is_patch_of_interest: 199 if applied: 200 # We applied the patch we wanted to, we can stop. 201 applied_patches.append(pe) 202 return True, applied_patches, failed_patches 203 else: 204 # We failed the patch we cared about, we can stop. 205 failed_patches.append(pe) 206 return False, applied_patches, failed_patches 207 else: 208 if applied: 209 applied_patches.append(pe) 210 elif meant_to_apply: 211 # Broke before we reached the patch we cared about. Stop. 212 failed_patches.append(pe) 213 return False, applied_patches, failed_patches 214 raise ValueError(f"Did not find patch {rel_patch_path}. " "Does it exist?") 215 216 217def PrintPatchResults(patch_info: patch_utils.PatchInfo): 218 """Prints the results of handling the patches of a package. 219 220 Args: 221 patch_info: A dataclass that has information on the patches. 222 """ 223 224 def _fmt(patches): 225 return (str(pe.patch_path()) for pe in patches) 226 227 if patch_info.applied_patches: 228 print("\nThe following patches applied successfully:") 229 print("\n".join(_fmt(patch_info.applied_patches))) 230 231 if patch_info.failed_patches: 232 print("\nThe following patches failed to apply:") 233 print("\n".join(_fmt(patch_info.failed_patches))) 234 235 if patch_info.non_applicable_patches: 236 print("\nThe following patches were not applicable:") 237 print("\n".join(_fmt(patch_info.non_applicable_patches))) 238 239 if patch_info.modified_metadata: 240 print( 241 "\nThe patch metadata file %s has been modified" 242 % os.path.basename(patch_info.modified_metadata) 243 ) 244 245 if patch_info.disabled_patches: 246 print("\nThe following patches were disabled:") 247 print("\n".join(_fmt(patch_info.disabled_patches))) 248 249 if patch_info.removed_patches: 250 print( 251 "\nThe following patches were removed from the patch metadata file:" 252 ) 253 for cur_patch_path in patch_info.removed_patches: 254 print("%s" % os.path.basename(cur_patch_path)) 255 256 257def main(sys_argv: List[str]): 258 """Applies patches to the source tree and takes action on a failed patch.""" 259 260 args_output = GetCommandLineArgs(sys_argv) 261 262 llvm_src_dir = Path(args_output.src_path) 263 if not llvm_src_dir.is_dir(): 264 raise ValueError(f"--src_path arg {llvm_src_dir} is not a directory") 265 patches_json_fp = Path(args_output.patch_metadata_file) 266 if not patches_json_fp.is_file(): 267 raise ValueError( 268 "--patch_metadata_file arg " f"{patches_json_fp} is not a file" 269 ) 270 271 def _apply_all(args): 272 if args.svn_version is None: 273 raise ValueError("--svn_version must be set when applying patches") 274 result = patch_utils.apply_all_from_json( 275 svn_version=args.svn_version, 276 llvm_src_dir=llvm_src_dir, 277 patches_json_fp=patches_json_fp, 278 patch_cmd=patch_utils.git_am 279 if args.git_am 280 else patch_utils.gnu_patch, 281 continue_on_failure=args.failure_mode 282 == failure_modes.FailureModes.CONTINUE, 283 ) 284 PrintPatchResults(result) 285 286 def _disable(args): 287 patch_cmd = patch_utils.git_am if args.git_am else patch_utils.gnu_patch 288 patch_utils.update_version_ranges( 289 args.svn_version, llvm_src_dir, patches_json_fp, patch_cmd 290 ) 291 292 def _test_single(args): 293 if not args.test_patch: 294 raise ValueError( 295 "Running with bisect_patches requires the " "--test_patch flag." 296 ) 297 svn_version = GetHEADSVNVersion(llvm_src_dir) 298 error_code = CheckPatchApplies( 299 svn_version, 300 llvm_src_dir, 301 patches_json_fp, 302 args.test_patch, 303 ) 304 # Since this is for bisection, we want to exit with the 305 # GitBisectionCode enum. 306 sys.exit(int(error_code)) 307 308 dispatch_table = { 309 failure_modes.FailureModes.FAIL: _apply_all, 310 failure_modes.FailureModes.CONTINUE: _apply_all, 311 failure_modes.FailureModes.DISABLE_PATCHES: _disable, 312 failure_modes.FailureModes.BISECT_PATCHES: _test_single, 313 } 314 315 if args_output.failure_mode in dispatch_table: 316 dispatch_table[args_output.failure_mode](args_output) 317 318 319if __name__ == "__main__": 320 main(sys.argv[1:]) 321