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