diff options
Diffstat (limited to 'llvm_tools/patch_manager.py')
-rwxr-xr-x | llvm_tools/patch_manager.py | 1010 |
1 files changed, 280 insertions, 730 deletions
diff --git a/llvm_tools/patch_manager.py b/llvm_tools/patch_manager.py index f2d6b322..4d4e8385 100755 --- a/llvm_tools/patch_manager.py +++ b/llvm_tools/patch_manager.py @@ -1,755 +1,305 @@ #!/usr/bin/env python3 -# -*- coding: utf-8 -*- -# Copyright 2019 The Chromium OS Authors. All rights reserved. +# 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.""" -from __future__ import print_function - import argparse -import json +import enum import os -import subprocess +from pathlib import Path import sys -from collections import namedtuple +from typing import Iterable, List, Optional, Tuple -import get_llvm_hash from failure_modes import FailureModes -from subprocess_helpers import check_call +import get_llvm_hash +import patch_utils from subprocess_helpers import check_output -def is_directory(dir_path): - """Validates that the argument passed into 'argparse' is a directory.""" - - if not os.path.isdir(dir_path): - raise ValueError('Path is not a directory: %s' % dir_path) - - return dir_path - - -def is_patch_metadata_file(patch_metadata_file): - """Valides the argument into 'argparse' is a patch file.""" - - if not os.path.isfile(patch_metadata_file): - raise ValueError( - 'Invalid patch metadata file provided: %s' % patch_metadata_file) - - if not patch_metadata_file.endswith('.json'): - raise ValueError( - 'Patch metadata file does not end in ".json": %s' % patch_metadata_file) - - return patch_metadata_file - - -def is_valid_failure_mode(failure_mode): - """Validates that the failure mode passed in is correct.""" - - cur_failure_modes = [mode.value for mode in FailureModes] - - if failure_mode not in cur_failure_modes: - raise ValueError('Invalid failure mode provided: %s' % failure_mode) - - return failure_mode - - -def EnsureBisectModeAndSvnVersionAreSpecifiedTogether(failure_mode, - good_svn_version): - """Validates that 'good_svn_version' is passed in only for bisection.""" - - if failure_mode != FailureModes.BISECT_PATCHES.value and good_svn_version: - raise ValueError('"good_svn_version" is only available for bisection.') - elif failure_mode == FailureModes.BISECT_PATCHES.value and \ - not good_svn_version: - raise ValueError('A good SVN version is required for bisection (used by' - '"git bisect start".') - - -def GetCommandLineArgs(): - """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 last good SVN version which is required by - # `git bisect start` (only valid for bisection mode). - parser.add_argument( - '--good_svn_version', - type=int, - help='INTERNAL USE ONLY... (used for bisection.)') - - # Add argument for the number of patches it iterate. Only used when performing - # `git bisect run`. - parser.add_argument( - '--num_patches_to_iterate', type=int, help=argparse.SUPPRESS) - - # Add argument for whether bisection should continue. Only used for - # 'bisect_patches.' - parser.add_argument( - '--continue_bisection', - type=bool, - default=False, - help='Determines whether bisection should continue after successfully ' - 'bisecting a patch (default: %(default)s) - only used for ' - '"bisect_patches"') - - # Trust src_path HEAD and svn_version. - parser.add_argument( - '--use_src_head', - action='store_true', - help='Use the HEAD of src_path directory as is, not necessarily the same ' - 'as the svn_version of upstream.') - - # Add argument for the LLVM version to use for patch management. - parser.add_argument( - '--svn_version', - type=int, - required=True, - help='the LLVM svn version to use for patch management (determines ' - 'whether a patch is applicable)') - - # Add argument for the patch metadata file that is in $FILESDIR. - parser.add_argument( - '--patch_metadata_file', - required=True, - type=is_patch_metadata_file, - 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 ebuild's $FILESDIR path. - # Example: '.../sys-devel/llvm/files/'. - parser.add_argument( - '--filesdir_path', - required=True, - type=is_directory, - help='the absolute path to the ebuild "files/" directory') - - # Add argument for the absolute path to the unpacked sources. - parser.add_argument( - '--src_path', - required=True, - type=is_directory, - 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=FailureModes.FAIL.value, - type=is_valid_failure_mode, - help='the mode of the patch manager when handling failed patches ' \ - '(default: %(default)s)') - - # Parse the command line. - args_output = parser.parse_args() - - EnsureBisectModeAndSvnVersionAreSpecifiedTogether( - args_output.failure_mode, args_output.good_svn_version) - - return args_output +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=FailureModes.FAIL, + type=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.", + ) + + # Parse the command line. + return parser.parse_args(sys_argv) def GetHEADSVNVersion(src_path): - """Gets the SVN version of HEAD in the src tree.""" - - cmd = ['git', '-C', src_path, 'rev-parse', 'HEAD'] - - git_hash = check_output(cmd) - - version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip()) - - return version - - -def VerifyHEADIsTheSameAsSVNVersion(src_path, svn_version): - """Verifies that HEAD's SVN version matches 'svn_version'.""" - - head_svn_version = GetHEADSVNVersion(src_path) - - if head_svn_version != svn_version: - raise ValueError('HEAD\'s SVN version %d does not match "svn_version"' - ' %d, please move HEAD to "svn_version"s\' git hash.' % - (head_svn_version, svn_version)) - - -def GetPathToPatch(filesdir_path, rel_patch_path): - """Gets the absolute path to a patch in $FILESDIR. - - Args: - filesdir_path: The absolute path to $FILESDIR. - rel_patch_path: The relative path to the patch in '$FILESDIR/'. - - Returns: - The absolute path to the patch in $FILESDIR. - - Raises: - ValueError: Unable to find the path to the patch in $FILESDIR. - """ - - if not os.path.isdir(filesdir_path): - raise ValueError('Invalid path to $FILESDIR provided: %s' % filesdir_path) - - # Combine $FILESDIR + relative path of patch to $FILESDIR. - patch_path = os.path.join(filesdir_path, rel_patch_path) - - if not os.path.isfile(patch_path): - raise ValueError('The absolute path %s to the patch %s does not exist' % - (patch_path, rel_patch_path)) - - return patch_path - - -def GetPatchMetadata(patch_dict): - """Gets the patch's metadata. - - Args: - patch_dict: A dictionary that has the patch metadata. - - Returns: - A tuple that contains the metadata values. - """ - - # Get the metadata values of a patch if possible. - # FIXME(b/221489531): Remove start_version & end_version - if 'version_range' in patch_dict: - start_version = patch_dict['version_range'].get('from', 0) - end_version = patch_dict['version_range'].get('until', None) - else: - start_version = patch_dict.get('start_version', 0) - end_version = patch_dict.get('end_version', None) - is_critical = patch_dict.get('is_critical', False) - - return start_version, end_version, is_critical - + """Gets the SVN version of HEAD in the src tree.""" -def ApplyPatch(src_path, patch_path): - """Attempts to apply the patch. + cmd = ["git", "-C", src_path, "rev-parse", "HEAD"] - Args: - src_path: The absolute path to the unpacked sources of the package. - patch_path: The absolute path to the patch in $FILESDIR/ + git_hash = check_output(cmd) - Returns: - A boolean where 'True' means that the patch applied fine or 'False' means - that the patch failed to apply. - """ + version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip()) - if not os.path.isdir(src_path): - raise ValueError('Invalid src path provided: %s' % src_path) - - if not os.path.isfile(patch_path): - raise ValueError('Invalid patch file provided: %s' % patch_path) - - # Test the patch with '--dry-run' before actually applying the patch. - test_patch_cmd = [ - 'patch', '--dry-run', '-d', src_path, '-f', '-p1', '-E', - '--no-backup-if-mismatch', '-i', patch_path - ] - - # Cmd to apply a patch in the src unpack path. - apply_patch_cmd = [ - 'patch', '-d', src_path, '-f', '-p1', '-E', '--no-backup-if-mismatch', - '-i', patch_path - ] - - try: - check_output(test_patch_cmd) - - # If the mode is 'continue', then catching the exception makes sure that - # the program does not exit on the first failed applicable patch. - except subprocess.CalledProcessError: - # Test run on the patch failed to apply. - return False - - # Test run succeeded on the patch. - check_output(apply_patch_cmd) - - return True - - -def UpdatePatchMetadataFile(patch_metadata_file, patches): - """Updates the .json file with unchanged and at least one changed patch. - - Args: - patch_metadata_file: The absolute path to the .json file that has all the - patches and its metadata. - patches: A list of patches whose metadata were or were not updated. - - Raises: - ValueError: The patch metadata file does not have the correct extension. - """ - - if not patch_metadata_file.endswith('.json'): - raise ValueError('File does not end in ".json": %s' % patch_metadata_file) - - with open(patch_metadata_file, 'w') as patch_file: - json.dump(patches, patch_file, indent=4, separators=(',', ': ')) + return version 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 PerformBisection(src_path, good_commit, bad_commit, svn_version, - patch_metadata_file, filesdir_path, num_patches): - """Performs bisection to determine where a patch stops applying.""" - - start_cmd = [ - 'git', '-C', src_path, 'bisect', 'start', bad_commit, good_commit - ] - - check_output(start_cmd) - - run_cmd = [ - 'git', '-C', src_path, 'bisect', 'run', - os.path.abspath(__file__), '--svn_version', - '%d' % svn_version, '--patch_metadata_file', patch_metadata_file, - '--filesdir_path', filesdir_path, '--src_path', src_path, - '--failure_mode', 'internal_bisection', '--num_patches_to_iterate', - '%d' % num_patches - ] - - check_call(run_cmd) - - # Successfully bisected the patch, so retrieve the SVN version from the - # commit message. - get_bad_commit_hash_cmd = [ - 'git', '-C', src_path, 'rev-parse', 'refs/bisect/bad' - ] - - git_hash = check_output(get_bad_commit_hash_cmd) - - end_cmd = ['git', '-C', src_path, 'bisect', 'reset'] - - check_output(end_cmd) - - # `git bisect run` returns the bad commit hash and the commit message. - version = get_llvm_hash.GetVersionFrom(src_path, git_hash.rstrip()) - - return version - - -def CleanSrcTree(src_path): - """Cleans the source tree of the changes made in 'src_path'.""" - - reset_src_tree_cmd = ['git', '-C', src_path, 'reset', 'HEAD', '--hard'] - - check_output(reset_src_tree_cmd) - - clean_src_tree_cmd = ['git', '-C', src_path, 'clean', '-fd'] - - check_output(clean_src_tree_cmd) - - -def SaveSrcTreeState(src_path): - """Stashes the changes made so far to the source tree.""" - - save_src_tree_cmd = ['git', '-C', src_path, 'stash', '-a'] - - check_output(save_src_tree_cmd) - - -def RestoreSrcTreeState(src_path, bad_commit_hash): - """Restores the changes made to the source tree.""" - - checkout_cmd = ['git', '-C', src_path, 'checkout', bad_commit_hash] - - check_output(checkout_cmd) - - get_changes_cmd = ['git', '-C', src_path, 'stash', 'pop'] - - check_output(get_changes_cmd) - - -def HandlePatches(svn_version, - patch_metadata_file, - filesdir_path, - src_path, - mode, - good_svn_version=None, - num_patches_to_iterate=None, - continue_bisection=False): - """Handles the patches in the .json file for the package. - - Args: - svn_version: The LLVM version to use for patch management. - patch_metadata_file: The absolute path to the .json file in '$FILESDIR/' - that has all the patches and their metadata. - filesdir_path: The absolute path to $FILESDIR. - src_path: The absolute path to the unpacked destination of the package. - mode: The action to take when an applicable patch failed to apply. - Ex: 'FailureModes.FAIL' - good_svn_version: Only used by 'bisect_patches' which tells - `git bisect start` the good version. - num_patches_to_iterate: The number of patches to iterate in the .JSON file - (internal use). Only used by `git bisect run`. - continue_bisection: Only used for 'bisect_patches' mode. If flag is set, - then bisection will continue to the next patch when successfully bisected a - patch. - - Returns: - Depending on the mode, 'None' would be returned if everything went well or - the .json file was not updated. Otherwise, a list or multiple lists would - be returned that indicates what has changed. - - Raises: - ValueError: The patch metadata file does not exist or does not end with - '.json' or the absolute path to $FILESDIR does not exist or the unpacked - path does not exist or if the mode is 'fail', then an applicable patch - failed to apply. - """ - - # A flag for whether the mode specified would possible modify the patches. - can_modify_patches = False - - # 'fail' or 'continue' mode would not modify a patch's metadata, so the .json - # file would stay the same. - if mode != FailureModes.FAIL and mode != FailureModes.CONTINUE: - can_modify_patches = True - - # A flag that determines whether at least one patch's metadata was - # updated due to the mode that is passed in. - updated_patch = False - - # A list of patches that will be in the updated .json file. - applicable_patches = [] - - # A list of patches that successfully applied. - applied_patches = [] - - # A list of patches that were disabled. - disabled_patches = [] - - # A list of bisected patches. - bisected_patches = [] - - # A list of non applicable patches. - non_applicable_patches = [] - - # A list of patches that will not be included in the updated .json file - removed_patches = [] - - # Whether the patch metadata file was modified where 'None' means that the - # patch metadata file was not modified otherwise the absolute path to the - # patch metadata file is stored. - modified_metadata = None - - # A list of patches that failed to apply. - failed_patches = [] - - with open(patch_metadata_file) as patch_file: - patch_file_contents = json.load(patch_file) - - if mode == FailureModes.BISECT_PATCHES: - # A good and bad commit are required by `git bisect start`. - good_commit, bad_commit = GetCommitHashesForBisection( - src_path, good_svn_version, svn_version) - - # Patch format: - # { - # "rel_patch_path" : "[REL_PATCH_PATH_FROM_$FILESDIR]" - # [PATCH_METADATA] if available. - # } - # - # For each patch, find the path to it in $FILESDIR and get its metadata if - # available, then check if the patch is applicable. - for patch_dict_index, cur_patch_dict in enumerate(patch_file_contents): - # Used by the internal bisection. All the patches in the interval [0, N] - # have been iterated. - if num_patches_to_iterate and \ - (patch_dict_index + 1) > num_patches_to_iterate: - break - - # Get the absolute path to the patch in $FILESDIR. - path_to_patch = GetPathToPatch(filesdir_path, - cur_patch_dict['rel_patch_path']) - - # Get the patch's metadata. - # - # Index information of 'patch_metadata': - # [0]: start_version - # [1]: end_version - # [2]: is_critical - patch_metadata = GetPatchMetadata(cur_patch_dict) - - if not patch_metadata[1]: - # Patch does not have an 'end_version' value which implies 'end_version' - # == 'inf' ('svn_version' will always be less than 'end_version'), so - # the patch is applicable if 'svn_version' >= 'start_version'. - patch_applicable = svn_version >= patch_metadata[0] - else: - # Patch is applicable if 'svn_version' >= 'start_version' && - # "svn_version" < "end_version". - patch_applicable = (svn_version >= patch_metadata[0] and \ - svn_version < patch_metadata[1]) - - if can_modify_patches: - # Add to the list only if the mode can potentially modify a patch. - # - # If the mode is 'remove_patches', then all patches that are - # applicable or are from the future will be added to the updated .json - # file and all patches that are not applicable will be added to the - # remove patches list which will not be included in the updated .json - # file. - if patch_applicable or svn_version < patch_metadata[0] or \ - mode != FailureModes.REMOVE_PATCHES: - applicable_patches.append(cur_patch_dict) - elif mode == FailureModes.REMOVE_PATCHES: - removed_patches.append(path_to_patch) - - if not modified_metadata: - # At least one patch will be removed from the .json file. - modified_metadata = patch_metadata_file - - if not patch_applicable: - non_applicable_patches.append(os.path.basename(path_to_patch)) - - # There is no need to apply patches in 'remove_patches' mode because the - # mode removes patches that do not apply anymore based off of - # 'svn_version.' - if patch_applicable and mode != FailureModes.REMOVE_PATCHES: - patch_applied = ApplyPatch(src_path, path_to_patch) - - if not patch_applied: # Failed to apply patch. - failed_patches.append(os.path.basename(path_to_patch)) - - # Check the mode to determine what action to take on the failing - # patch. - if mode == FailureModes.DISABLE_PATCHES: - # Set the patch's 'end_version' to 'svn_version' so the patch - # would not be applicable anymore (i.e. the patch's 'end_version' - # would not be greater than 'svn_version'). - - # Last element in 'applicable_patches' is the current patch. - applicable_patches[-1]['end_version'] = svn_version - - disabled_patches.append(os.path.basename(path_to_patch)) - - if not updated_patch: - # At least one patch has been modified, so the .json file - # will be updated with the new patch metadata. - updated_patch = True - - modified_metadata = patch_metadata_file - elif mode == FailureModes.BISECT_PATCHES: - # Figure out where the patch's stops applying and set the patch's - # 'end_version' to that version. - - # Do not want to overwrite the changes to the current progress of - # 'bisect_patches' on the source tree. - SaveSrcTreeState(src_path) - - # Need a clean source tree for `git bisect run` to avoid unnecessary - # fails for patches. - CleanSrcTree(src_path) - - print('\nStarting to bisect patch %s for SVN version %d:\n' % - (os.path.basename(cur_patch_dict['rel_patch_path']), - svn_version)) - - # Performs the bisection: calls `git bisect start` and - # `git bisect run`, where `git bisect run` is going to call this - # script as many times as needed with specific arguments. - bad_svn_version = PerformBisection( - src_path, good_commit, bad_commit, svn_version, - patch_metadata_file, filesdir_path, patch_dict_index + 1) - - print('\nSuccessfully bisected patch %s, starts to fail to apply ' - 'at %d\n' % (os.path.basename( - cur_patch_dict['rel_patch_path']), bad_svn_version)) - - # Overwrite the .JSON file with the new 'end_version' for the - # current failed patch so that if there are other patches that - # fail to apply, then the 'end_version' for the current patch could - # be applicable when `git bisect run` is performed on the next - # failed patch because the same .JSON file is used for `git bisect - # run`. - patch_file_contents[patch_dict_index][ - 'end_version'] = bad_svn_version - UpdatePatchMetadataFile(patch_metadata_file, patch_file_contents) - - # Clear the changes made to the source tree by `git bisect run`. - CleanSrcTree(src_path) - - if not continue_bisection: - # Exiting program early because 'continue_bisection' is not set. - sys.exit(0) - - bisected_patches.append( - '%s starts to fail to apply at %d' % (os.path.basename( - cur_patch_dict['rel_patch_path']), bad_svn_version)) - - # Continue where 'bisect_patches' left off. - RestoreSrcTreeState(src_path, bad_commit) - - if not modified_metadata: - # At least one patch's 'end_version' has been updated. - modified_metadata = patch_metadata_file - - elif mode == FailureModes.FAIL: - if applied_patches: - print('The following patches applied successfully up to the ' - 'failed patch:') - print('\n'.join(applied_patches)) - - # Throw an exception on the first patch that failed to apply. + """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, + ) + 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, +) -> 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 = [] + 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, 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, + continue_on_failure=args.failure_mode == FailureModes.CONTINUE, + ) + PrintPatchResults(result) + + def _remove(args): + patch_utils.remove_old_patches( + args.svn_version, llvm_src_dir, patches_json_fp + ) + + def _disable(args): + patch_utils.update_version_ranges( + args.svn_version, llvm_src_dir, patches_json_fp + ) + + def _test_single(args): + if not args.test_patch: raise ValueError( - 'Failed to apply patch: %s' % os.path.basename(path_to_patch)) - elif mode == FailureModes.INTERNAL_BISECTION: - # Determine the exit status for `git bisect run` because of the - # failed patch in the interval [0, N]. - # - # NOTE: `git bisect run` exit codes are as follows: - # 130: Terminates the bisection. - # 1: Similar as `git bisect bad`. - - # Some patch in the interval [0, N) failed, so terminate bisection - # (the patch stack is broken). - if (patch_dict_index + 1) != num_patches_to_iterate: - print('\nTerminating bisection due to patch %s failed to apply ' - 'on SVN version %d.\n' % (os.path.basename( - cur_patch_dict['rel_patch_path']), svn_version)) - - # Man page for `git bisect run` states that any value over 127 - # terminates it. - sys.exit(130) - - # Changes to the source tree need to be removed, otherwise some - # patches may fail when applying the patch to the source tree when - # `git bisect run` calls this script again. - CleanSrcTree(src_path) - - # The last patch in the interval [0, N] failed to apply, so let - # `git bisect run` know that the last patch (the patch that failed - # originally which led to `git bisect run` to be invoked) is bad - # with exit code 1. - sys.exit(1) - else: # Successfully applied patch - applied_patches.append(os.path.basename(path_to_patch)) - - # All patches in the interval [0, N] applied successfully, so let - # `git bisect run` know that the program exited with exit code 0 (good). - if mode == FailureModes.INTERNAL_BISECTION: - # Changes to the source tree need to be removed, otherwise some - # patches may fail when applying the patch to the source tree when - # `git bisect run` calls this script again. - # - # Also, if `git bisect run` will NOT call this script again (terminated) and - # if the source tree changes are not removed, `git bisect reset` will - # complain that the changes would need to be 'stashed' or 'removed' in - # order to reset HEAD back to the bad commit's git hash, so HEAD will remain - # on the last git hash used by `git bisect run`. - CleanSrcTree(src_path) - - # NOTE: Exit code 0 is similar to `git bisect good`. - sys.exit(0) - - # Create a namedtuple of the patch results. - PatchInfo = namedtuple('PatchInfo', [ - 'applied_patches', 'failed_patches', 'non_applicable_patches', - 'disabled_patches', 'removed_patches', 'modified_metadata' - ]) - - patch_info = PatchInfo( - applied_patches=applied_patches, - failed_patches=failed_patches, - non_applicable_patches=non_applicable_patches, - disabled_patches=disabled_patches, - removed_patches=removed_patches, - modified_metadata=modified_metadata) - - # Determine post actions after iterating through the patches. - if mode == FailureModes.REMOVE_PATCHES: - if removed_patches: - UpdatePatchMetadataFile(patch_metadata_file, applicable_patches) - elif mode == FailureModes.DISABLE_PATCHES: - if updated_patch: - UpdatePatchMetadataFile(patch_metadata_file, applicable_patches) - elif mode == FailureModes.BISECT_PATCHES: - PrintPatchResults(patch_info) - if modified_metadata: - print('\nThe following patches have been bisected:') - print('\n'.join(bisected_patches)) - - # Exiting early because 'bisect_patches' will not be called from other - # scripts, only this script uses 'bisect_patches'. The intent is to provide - # bisection information on the patches and aid in the bisection process. - sys.exit(0) - - return patch_info - - -def PrintPatchResults(patch_info): - """Prints the results of handling the patches of a package. - - Args: - patch_info: A namedtuple that has information on the patches. - """ - - if patch_info.applied_patches: - print('\nThe following patches applied successfully:') - print('\n'.join(patch_info.applied_patches)) - - if patch_info.failed_patches: - print('\nThe following patches failed to apply:') - print('\n'.join(patch_info.failed_patches)) - - if patch_info.non_applicable_patches: - print('\nThe following patches were not applicable:') - print('\n'.join(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(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(): - """Applies patches to the source tree and takes action on a failed patch.""" - - args_output = GetCommandLineArgs() - - if args_output.failure_mode != FailureModes.INTERNAL_BISECTION.value: - # If the SVN version of HEAD is not the same as 'svn_version', then some - # patches that fail to apply could successfully apply if HEAD's SVN version - # was the same as 'svn_version'. In other words, HEAD's git hash should be - # what is being updated to (e.g. LLVM_NEXT_HASH). - if not args_output.use_src_head: - VerifyHEADIsTheSameAsSVNVersion(args_output.src_path, - args_output.svn_version) - else: - # `git bisect run` called this script. - # - # `git bisect run` moves HEAD each time it invokes this script, so set the - # 'svn_version' to be current HEAD's SVN version so that the previous - # SVN version is not used in determining whether a patch is applicable. - args_output.svn_version = GetHEADSVNVersion(args_output.src_path) - - # Get the results of handling the patches of the package. - patch_info = HandlePatches( - args_output.svn_version, args_output.patch_metadata_file, - args_output.filesdir_path, args_output.src_path, - FailureModes(args_output.failure_mode), args_output.good_svn_version, - args_output.num_patches_to_iterate, args_output.continue_bisection) - - PrintPatchResults(patch_info) - - -if __name__ == '__main__': - main() + "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 = { + FailureModes.FAIL: _apply_all, + FailureModes.CONTINUE: _apply_all, + FailureModes.REMOVE_PATCHES: _remove, + FailureModes.DISABLE_PATCHES: _disable, + 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:]) |