You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
466 lines
14 KiB
466 lines
14 KiB
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
# 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.
|
|
|
|
"""Performs bisection on LLVM based off a .JSON file."""
|
|
|
|
|
|
import argparse
|
|
import enum
|
|
import errno
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
|
|
import chroot
|
|
import get_llvm_hash
|
|
import git_llvm_rev
|
|
import modify_a_tryjob
|
|
import update_chromeos_llvm_hash
|
|
import update_tryjob_status
|
|
|
|
|
|
class BisectionExitStatus(enum.Enum):
|
|
"""Exit code when performing bisection."""
|
|
|
|
# Means that there are no more revisions available to bisect.
|
|
BISECTION_COMPLETE = 126
|
|
|
|
|
|
def GetCommandLineArgs():
|
|
"""Parses the command line for the command line arguments."""
|
|
|
|
# Default path to the chroot if a path is not specified.
|
|
cros_root = os.path.expanduser("~")
|
|
cros_root = os.path.join(cros_root, "chromiumos")
|
|
|
|
# Create parser and add optional command-line arguments.
|
|
parser = argparse.ArgumentParser(
|
|
description="Bisects LLVM via tracking a JSON file."
|
|
)
|
|
|
|
# Add argument for other change lists that want to run alongside the tryjob
|
|
# which has a change list of updating a package's git hash.
|
|
parser.add_argument(
|
|
"--parallel",
|
|
type=int,
|
|
default=3,
|
|
help="How many tryjobs to create between the last good version and "
|
|
"the first bad version (default: %(default)s)",
|
|
)
|
|
|
|
# Add argument for the good LLVM revision for bisection.
|
|
parser.add_argument(
|
|
"--start_rev",
|
|
required=True,
|
|
type=int,
|
|
help="The good revision for the bisection.",
|
|
)
|
|
|
|
# Add argument for the bad LLVM revision for bisection.
|
|
parser.add_argument(
|
|
"--end_rev",
|
|
required=True,
|
|
type=int,
|
|
help="The bad revision for the bisection.",
|
|
)
|
|
|
|
# Add argument for the absolute path to the file that contains information on
|
|
# the previous tested svn version.
|
|
parser.add_argument(
|
|
"--last_tested",
|
|
required=True,
|
|
help="the absolute path to the file that contains the tryjobs",
|
|
)
|
|
|
|
# Add argument for the absolute path to the LLVM source tree.
|
|
parser.add_argument(
|
|
"--src_path",
|
|
help="the path to the LLVM source tree to use (used for retrieving the "
|
|
"git hash of each version between the last good version and first bad "
|
|
"version)",
|
|
)
|
|
|
|
# Add argument for other change lists that want to run alongside the tryjob
|
|
# which has a change list of updating a package's git hash.
|
|
parser.add_argument(
|
|
"--extra_change_lists",
|
|
type=int,
|
|
nargs="+",
|
|
help="change lists that would like to be run alongside the change list "
|
|
"of updating the packages",
|
|
)
|
|
|
|
# Add argument for custom options for the tryjob.
|
|
parser.add_argument(
|
|
"--options",
|
|
required=False,
|
|
nargs="+",
|
|
help="options to use for the tryjob testing",
|
|
)
|
|
|
|
# Add argument for the builder to use for the tryjob.
|
|
parser.add_argument(
|
|
"--builder", required=True, help="builder to use for the tryjob testing"
|
|
)
|
|
|
|
# Add argument for the description of the tryjob.
|
|
parser.add_argument(
|
|
"--description",
|
|
required=False,
|
|
nargs="+",
|
|
help="the description of the tryjob",
|
|
)
|
|
|
|
# Add argument for a specific chroot path.
|
|
parser.add_argument(
|
|
"--chroot_path",
|
|
default=cros_root,
|
|
help="the path to the chroot (default: %(default)s)",
|
|
)
|
|
|
|
# Add argument for whether to display command contents to `stdout`.
|
|
parser.add_argument(
|
|
"--verbose",
|
|
action="store_true",
|
|
help="display contents of a command to the terminal "
|
|
"(default: %(default)s)",
|
|
)
|
|
|
|
# Add argument for whether to display command contents to `stdout`.
|
|
parser.add_argument(
|
|
"--nocleanup",
|
|
action="store_false",
|
|
dest="cleanup",
|
|
help="Abandon CLs created for bisectoin",
|
|
)
|
|
|
|
args_output = parser.parse_args()
|
|
|
|
assert (
|
|
args_output.start_rev < args_output.end_rev
|
|
), "Start revision %d is >= end revision %d" % (
|
|
args_output.start_rev,
|
|
args_output.end_rev,
|
|
)
|
|
|
|
if args_output.last_tested and not args_output.last_tested.endswith(
|
|
".json"
|
|
):
|
|
raise ValueError(
|
|
'Filed provided %s does not end in ".json"'
|
|
% args_output.last_tested
|
|
)
|
|
|
|
return args_output
|
|
|
|
|
|
def GetRemainingRange(start, end, tryjobs):
|
|
"""Gets the start and end intervals in 'json_file'.
|
|
|
|
Args:
|
|
start: The start version of the bisection provided via the command line.
|
|
end: The end version of the bisection provided via the command line.
|
|
tryjobs: A list of tryjobs where each element is in the following format:
|
|
[
|
|
{[TRYJOB_INFORMATION]},
|
|
{[TRYJOB_INFORMATION]},
|
|
...,
|
|
{[TRYJOB_INFORMATION]}
|
|
]
|
|
|
|
Returns:
|
|
The new start version and end version for bisection, a set of revisions
|
|
that are 'pending' and a set of revisions that are to be skipped.
|
|
|
|
Raises:
|
|
ValueError: The value for 'status' is missing or there is a mismatch
|
|
between 'start' and 'end' compared to the 'start' and 'end' in the JSON
|
|
file.
|
|
AssertionError: The new start version is >= than the new end version.
|
|
"""
|
|
|
|
if not tryjobs:
|
|
return start, end, {}, {}
|
|
|
|
# Verify that each tryjob has a value for the 'status' key.
|
|
for cur_tryjob_dict in tryjobs:
|
|
if not cur_tryjob_dict.get("status", None):
|
|
raise ValueError(
|
|
'"status" is missing or has no value, please '
|
|
"go to %s and update it" % cur_tryjob_dict["link"]
|
|
)
|
|
|
|
all_bad_revisions = [end]
|
|
all_bad_revisions.extend(
|
|
cur_tryjob["rev"]
|
|
for cur_tryjob in tryjobs
|
|
if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.BAD.value
|
|
)
|
|
|
|
# The minimum value for the 'bad' field in the tryjobs is the new end
|
|
# version.
|
|
bad_rev = min(all_bad_revisions)
|
|
|
|
all_good_revisions = [start]
|
|
all_good_revisions.extend(
|
|
cur_tryjob["rev"]
|
|
for cur_tryjob in tryjobs
|
|
if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.GOOD.value
|
|
)
|
|
|
|
# The maximum value for the 'good' field in the tryjobs is the new start
|
|
# version.
|
|
good_rev = max(all_good_revisions)
|
|
|
|
# The good version should always be strictly less than the bad version;
|
|
# otherwise, bisection is broken.
|
|
assert (
|
|
good_rev < bad_rev
|
|
), "Bisection is broken because %d (good) is >= " "%d (bad)" % (
|
|
good_rev,
|
|
bad_rev,
|
|
)
|
|
|
|
# Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'.
|
|
#
|
|
# NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
|
|
# that have already been launched (this set is used when constructing the
|
|
# list of revisions to launch tryjobs for).
|
|
pending_revisions = {
|
|
tryjob["rev"]
|
|
for tryjob in tryjobs
|
|
if tryjob["status"] == update_tryjob_status.TryjobStatus.PENDING.value
|
|
and good_rev < tryjob["rev"] < bad_rev
|
|
}
|
|
|
|
# Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'.
|
|
#
|
|
# NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev'
|
|
# that have already been marked as 'skip' (this set is used when constructing
|
|
# the list of revisions to launch tryjobs for).
|
|
skip_revisions = {
|
|
tryjob["rev"]
|
|
for tryjob in tryjobs
|
|
if tryjob["status"] == update_tryjob_status.TryjobStatus.SKIP.value
|
|
and good_rev < tryjob["rev"] < bad_rev
|
|
}
|
|
|
|
return good_rev, bad_rev, pending_revisions, skip_revisions
|
|
|
|
|
|
def GetCommitsBetween(
|
|
start, end, parallel, src_path, pending_revisions, skip_revisions
|
|
):
|
|
"""Determines the revisions between start and end."""
|
|
|
|
with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir:
|
|
# We have guaranteed contiguous revision numbers after this,
|
|
# and that guarnatee simplifies things considerably, so we don't
|
|
# support anything before it.
|
|
assert (
|
|
start >= git_llvm_rev.base_llvm_revision
|
|
), f"{start} was too long ago"
|
|
|
|
with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo:
|
|
if not src_path:
|
|
src_path = new_repo
|
|
index_step = (end - (start + 1)) // (parallel + 1)
|
|
if not index_step:
|
|
index_step = 1
|
|
revisions = [
|
|
rev
|
|
for rev in range(start + 1, end, index_step)
|
|
if rev not in pending_revisions and rev not in skip_revisions
|
|
]
|
|
git_hashes = [
|
|
get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions
|
|
]
|
|
return revisions, git_hashes
|
|
|
|
|
|
def Bisect(
|
|
revisions,
|
|
git_hashes,
|
|
bisect_state,
|
|
last_tested,
|
|
update_packages,
|
|
chroot_path,
|
|
patch_metadata_file,
|
|
extra_change_lists,
|
|
options,
|
|
builder,
|
|
verbose,
|
|
):
|
|
"""Adds tryjobs and updates the status file with the new tryjobs."""
|
|
|
|
try:
|
|
for svn_revision, git_hash in zip(revisions, git_hashes):
|
|
tryjob_dict = modify_a_tryjob.AddTryjob(
|
|
update_packages,
|
|
git_hash,
|
|
svn_revision,
|
|
chroot_path,
|
|
patch_metadata_file,
|
|
extra_change_lists,
|
|
options,
|
|
builder,
|
|
verbose,
|
|
svn_revision,
|
|
)
|
|
|
|
bisect_state["jobs"].append(tryjob_dict)
|
|
finally:
|
|
# Do not want to lose progress if there is an exception.
|
|
if last_tested:
|
|
new_file = "%s.new" % last_tested
|
|
with open(new_file, "w") as json_file:
|
|
json.dump(
|
|
bisect_state, json_file, indent=4, separators=(",", ": ")
|
|
)
|
|
|
|
os.rename(new_file, last_tested)
|
|
|
|
|
|
def LoadStatusFile(last_tested, start, end):
|
|
"""Loads the status file for bisection."""
|
|
|
|
try:
|
|
with open(last_tested) as f:
|
|
return json.load(f)
|
|
except IOError as err:
|
|
if err.errno != errno.ENOENT:
|
|
raise
|
|
|
|
return {"start": start, "end": end, "jobs": []}
|
|
|
|
|
|
def main(args_output):
|
|
"""Bisects LLVM commits.
|
|
|
|
Raises:
|
|
AssertionError: The script was run inside the chroot.
|
|
"""
|
|
|
|
chroot.VerifyOutsideChroot()
|
|
patch_metadata_file = "PATCHES.json"
|
|
start = args_output.start_rev
|
|
end = args_output.end_rev
|
|
|
|
bisect_state = LoadStatusFile(args_output.last_tested, start, end)
|
|
if start != bisect_state["start"] or end != bisect_state["end"]:
|
|
raise ValueError(
|
|
f"The start {start} or the end {end} version provided is "
|
|
f'different than "start" {bisect_state["start"]} or "end" '
|
|
f'{bisect_state["end"]} in the .JSON file'
|
|
)
|
|
|
|
# Pending and skipped revisions are between 'start_rev' and 'end_rev'.
|
|
start_rev, end_rev, pending_revs, skip_revs = GetRemainingRange(
|
|
start, end, bisect_state["jobs"]
|
|
)
|
|
|
|
revisions, git_hashes = GetCommitsBetween(
|
|
start_rev,
|
|
end_rev,
|
|
args_output.parallel,
|
|
args_output.src_path,
|
|
pending_revs,
|
|
skip_revs,
|
|
)
|
|
|
|
# No more revisions between 'start_rev' and 'end_rev', so
|
|
# bisection is complete.
|
|
#
|
|
# This is determined by finding all valid revisions between 'start_rev'
|
|
# and 'end_rev' and that are NOT in the 'pending' and 'skipped' set.
|
|
if not revisions:
|
|
if pending_revs:
|
|
# Some tryjobs are not finished which may change the actual bad
|
|
# commit/revision when those tryjobs are finished.
|
|
no_revisions_message = (
|
|
f"No revisions between start {start_rev} "
|
|
f"and end {end_rev} to create tryjobs\n"
|
|
)
|
|
|
|
if pending_revs:
|
|
no_revisions_message += (
|
|
"The following tryjobs are pending:\n"
|
|
+ "\n".join(str(rev) for rev in pending_revs)
|
|
+ "\n"
|
|
)
|
|
|
|
if skip_revs:
|
|
no_revisions_message += (
|
|
"The following tryjobs were skipped:\n"
|
|
+ "\n".join(str(rev) for rev in skip_revs)
|
|
+ "\n"
|
|
)
|
|
|
|
raise ValueError(no_revisions_message)
|
|
|
|
print(f"Finished bisecting for {args_output.last_tested}")
|
|
if args_output.src_path:
|
|
bad_llvm_hash = get_llvm_hash.GetGitHashFrom(
|
|
args_output.src_path, end_rev
|
|
)
|
|
else:
|
|
bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_rev)
|
|
print(
|
|
f"The bad revision is {end_rev} and its commit hash is "
|
|
f"{bad_llvm_hash}"
|
|
)
|
|
if skip_revs:
|
|
skip_revs_message = (
|
|
"\nThe following revisions were skipped:\n"
|
|
+ "\n".join(str(rev) for rev in skip_revs)
|
|
)
|
|
print(skip_revs_message)
|
|
|
|
if args_output.cleanup:
|
|
# Abandon all the CLs created for bisection
|
|
gerrit = os.path.join(
|
|
args_output.chroot_path, "chromite/bin/gerrit"
|
|
)
|
|
for build in bisect_state["jobs"]:
|
|
try:
|
|
subprocess.check_output(
|
|
[gerrit, "abandon", str(build["cl"])],
|
|
stderr=subprocess.STDOUT,
|
|
encoding="utf-8",
|
|
)
|
|
except subprocess.CalledProcessError as err:
|
|
# the CL may have been abandoned
|
|
if "chromite.lib.gob_util.GOBError" not in err.output:
|
|
raise
|
|
|
|
return BisectionExitStatus.BISECTION_COMPLETE.value
|
|
|
|
for rev in revisions:
|
|
if (
|
|
update_tryjob_status.FindTryjobIndex(rev, bisect_state["jobs"])
|
|
is not None
|
|
):
|
|
raise ValueError(f'Revision {rev} exists already in "jobs"')
|
|
|
|
Bisect(
|
|
revisions,
|
|
git_hashes,
|
|
bisect_state,
|
|
args_output.last_tested,
|
|
update_chromeos_llvm_hash.DEFAULT_PACKAGES,
|
|
args_output.chroot_path,
|
|
patch_metadata_file,
|
|
args_output.extra_change_lists,
|
|
args_output.options,
|
|
args_output.builder,
|
|
args_output.verbose,
|
|
)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main(GetCommandLineArgs()))
|