blob: eba3a53085bb4d3dc251be4dba8d6f16c810af14 [file] [log] [blame]
#!/usr/bin/env python3
# Copyright 2019 The ChromiumOS Authors. All rights reserved.
# 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 dataclasses
import json
import os
from pathlib import Path
import subprocess
import sys
from typing import Any, Dict, IO, List, Optional, Tuple
from failure_modes import FailureModes
import get_llvm_hash
import patch_utils
from subprocess_helpers import check_call
from subprocess_helpers import check_output
@dataclasses.dataclass(frozen=True)
class PatchInfo:
"""Holds info for a round of patch applications."""
# str types are legacy. Patch lists should
# probably be PatchEntries,
applied_patches: List[patch_utils.PatchEntry]
failed_patches: List[patch_utils.PatchEntry]
# Can be deleted once legacy code is removed.
non_applicable_patches: List[str]
# Can be deleted once legacy code is removed.
disabled_patches: List[str]
# Can be deleted once legacy code is removed.
removed_patches: List[str]
# Can be deleted once legacy code is removed.
modified_metadata: Optional[str]
def _asdict(self):
return dataclasses.asdict(self)
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,
type=FailureModes,
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
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.
"""
if 'version_range' in patch_dict:
from_version = patch_dict['version_range'].get('from', 0)
until_version = patch_dict['version_range'].get('until', None)
else:
from_version = 0
until_version = None
is_critical = patch_dict.get('is_critical', False)
return from_version, until_version, is_critical
def ApplyPatch(src_path, patch_path):
"""Attempts to apply the patch.
Args:
src_path: The absolute path to the unpacked sources of the package.
patch_path: The absolute path to the patch in $FILESDIR/
Returns:
A boolean where 'True' means that the patch applied fine or 'False' means
that the patch failed to apply.
"""
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:
_WriteJsonChanges(patches, patch_file)
def _WriteJsonChanges(patches: List[Dict[str, Any]], file_io: IO[str]):
"""Write JSON changes to file, does not acquire new file lock."""
json.dump(patches, file_io, indent=4, separators=(',', ': '))
# Need to add a newline as json.dump omits it.
file_io.write('\n')
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 ApplyAllFromJson(svn_version: int,
llvm_src_dir: Path,
patches_json_fp: Path,
continue_on_failure: bool = False) -> PatchInfo:
"""Attempt to apply some patches to a given LLVM source tree.
This relies on a PATCHES.json file to be the primary way
the patches are applied.
Args:
svn_version: LLVM Subversion revision to patch.
llvm_src_dir: llvm-project root-level source directory to patch.
patches_json_fp: Filepath to the PATCHES.json file.
continue_on_failure: Skip any patches which failed to apply,
rather than throw an Exception.
"""
with patches_json_fp.open(encoding='utf-8') as f:
patches = patch_utils.json_to_patch_entries(patches_json_fp.parent, f)
skipped_patches = []
failed_patches = []
applied_patches = []
for pe in patches:
applied, failed_hunks = ApplySinglePatchEntry(svn_version, llvm_src_dir,
pe)
if applied:
applied_patches.append(pe)
continue
if failed_hunks is not None:
if continue_on_failure:
failed_patches.append(pe)
continue
else:
_PrintFailedPatch(pe, failed_hunks)
raise RuntimeError('failed to apply patch '
f'{pe.patch_path()}: {pe.title()}')
# Didn't apply, didn't fail, it was skipped.
skipped_patches.append(pe)
return PatchInfo(
non_applicable_patches=skipped_patches,
applied_patches=applied_patches,
failed_patches=failed_patches,
disabled_patches=[],
removed_patches=[],
modified_metadata=None,
)
def ApplySinglePatchEntry(
svn_version: int, llvm_src_dir: Path, pe: patch_utils.PatchEntry
) -> Tuple[bool, Optional[Dict[str, List[patch_utils.Hunk]]]]:
"""Try to apply a single PatchEntry object.
Returns:
Tuple where the first element indicates whether the patch applied,
and the second element is a faild hunk mapping from file name to lists of
hunks (if the patch didn't apply).
"""
# Don't apply patches outside of the version range.
if not pe.can_patch_version(svn_version):
return False, None
# Test first to avoid making changes.
test_application = pe.test_apply(llvm_src_dir)
if not test_application:
return False, test_application.failed_hunks
# Now actually make changes.
application_result = pe.apply(llvm_src_dir)
if not application_result:
# This should be very rare/impossible.
return False, application_result.failed_hunks
return True, None
def RemoveOldPatches(svn_version: int, llvm_src_dir: Path,
patches_json_fp: Path):
"""Remove patches that don't and will never apply for the future.
Patches are determined to be "old" via the "is_old" method for
each patch entry.
Args:
svn_version: LLVM SVN version.
llvm_src_dir: LLVM source directory.
patches_json_fp: Location to edit patches on.
"""
with patches_json_fp.open(encoding='utf-8') as f:
patches_list = json.load(f)
patch_entries = (patch_utils.PatchEntry.from_dict(llvm_src_dir, elem)
for elem in patches_list)
oldness = [(entry, entry.is_old(svn_version)) for entry in patch_entries]
filtered_entries = [entry.to_dict() for entry, old in oldness if not old]
with patch_utils.atomic_write(patches_json_fp, encoding='utf-8') as f:
_WriteJsonChanges(filtered_entries, f)
removed_entries = [entry for entry, old in oldness if old]
plural_patches = 'patch' if len(removed_entries) == 1 else 'patches'
print(f'Removed {len(removed_entries)} old {plural_patches}:')
for r in removed_entries:
print(f'- {r.rel_patch_path}: {r.title()}')
def UpdateVersionRanges(svn_version: int, llvm_src_dir: Path,
patches_json_fp: Path):
"""Reduce the version ranges of failing patches.
Patches which fail to apply will have their 'version_range.until'
field reduced to the passed in svn_version.
Modifies the contents of patches_json_fp.
Ars:
svn_version: LLVM revision number.
llvm_src_dir: llvm-project directory path.
patches_json_fp: Filepath to the PATCHES.json file.
"""
if IsGitDirty(llvm_src_dir):
raise RuntimeError('Cannot test patch applications, llvm_src_dir is dirty')
with patches_json_fp.open(encoding='utf-8') as f:
patch_entries = patch_utils.json_to_patch_entries(patches_json_fp.parent,
f)
modified_entries: List[patch_utils.PatchEntry] = []
for pe in patch_entries:
test_result = pe.test_apply(llvm_src_dir)
if not test_result:
if pe.version_range is None:
pe.version_range = {}
pe.version_range['until'] = svn_version
modified_entries.append(pe)
else:
# We have to actually apply the patch so that future patches
# will stack properly.
if not pe.apply(llvm_src_dir).succeeded:
CleanSrcTree(llvm_src_dir)
raise RuntimeError('Could not apply patch that dry ran successfully')
with patch_utils.atomic_write(patches_json_fp, encoding='utf-8') as f:
_WriteJsonChanges([p.to_dict() for p in patch_entries], f)
for entry in modified_entries:
print(f'Stopped applying {entry.rel_patch_path} ({entry.title()}) '
f'for r{svn_version}')
CleanSrcTree(llvm_src_dir)
def IsGitDirty(git_root_dir: Path) -> bool:
"""Return whether the given git directory has uncommitted changes."""
if not git_root_dir.is_dir():
raise ValueError(f'git_root_dir {git_root_dir} is not a directory')
cmd = ['git', 'ls-files', '-m', '--other', '--exclude-standard']
return (subprocess.run(cmd,
stdout=subprocess.PIPE,
check=True,
cwd=git_root_dir,
encoding='utf-8').stdout != "")
def _PrintFailedPatch(pe: patch_utils.PatchEntry,
failed_hunks: Dict[str, List[patch_utils.Hunk]]):
"""Print information about a single failing PatchEntry.
Args:
pe: A PatchEntry that failed.
failed_hunks: Hunks for pe which failed as dict:
filepath: [Hunk...]
"""
print(f'Could not apply {pe.rel_patch_path}: {pe.title()}', file=sys.stderr)
for fp, hunks in failed_hunks.items():
print(f'{fp}:', file=sys.stderr)
for h in hunks:
print(
f'- {pe.rel_patch_path} '
f'l:{h.patch_hunk_lineno_begin}...{h.patch_hunk_lineno_end}',
file=sys.stderr)
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]: from_version
# [1]: until_version
# [2]: is_critical
patch_metadata = GetPatchMetadata(cur_patch_dict)
if not patch_metadata[1]:
# Patch does not have an 'until' value which implies
# 'until' == 'inf' ('svn_version' will always be less
# than 'until'), so the patch is applicable if
# 'svn_version' >= 'from'.
patch_applicable = svn_version >= patch_metadata[0]
else:
# Patch is applicable if 'svn_version' >= 'from' &&
# "svn_version" < "until".
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 'until' to 'svn_version' so the patch
# would not be applicable anymore (i.e. the patch's 'until'
# would not be greater than 'svn_version').
# Last element in 'applicable_patches' is the current patch.
new_version_range = applicable_patches[-1].get('version_range', {})
new_version_range['until'] = svn_version
applicable_patches[-1]['version_range'] = new_version_range
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
# 'until' 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 'until' for the
# current failed patch so that if there are other patches that
# fail to apply, then the 'until' 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`.
new_version_range = patch_file_contents[patch_dict_index].get(
'version_range', {})
new_version_range['until'] = bad_svn_version
patch_file_contents[patch_dict_index][
'version_range'] = new_version_range
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 'until' 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.
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)
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: 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():
"""Applies patches to the source tree and takes action on a failed patch."""
args_output = GetCommandLineArgs()
if args_output.failure_mode != FailureModes.INTERNAL_BISECTION:
# 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)
def _apply_all(args):
result = ApplyAllFromJson(
svn_version=args.svn_version,
llvm_src_dir=Path(args.src_path),
patches_json_fp=Path(args.patch_metadata_file),
continue_on_failure=args.failure_mode == FailureModes.CONTINUE)
PrintPatchResults(result)
def _remove(args):
RemoveOldPatches(args.svn_version, Path(args.src_path),
Path(args.patch_metadata_file))
def _disable(args):
UpdateVersionRanges(args.svn_version, Path(args.src_path),
Path(args.patch_metadata_file))
dispatch_table = {
FailureModes.FAIL: _apply_all,
FailureModes.CONTINUE: _apply_all,
FailureModes.REMOVE_PATCHES: _remove,
FailureModes.DISABLE_PATCHES: _disable
}
if args_output.failure_mode in dispatch_table:
dispatch_table[args_output.failure_mode](args_output)
else:
# TODO(ajordanr): Legacy mode, remove when dispatch_table
# supports bisection.
# 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()