1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2019 The ChromiumOS Authors 3*760c253cSXin Li# Use of this source code is governed by a BSD-style license that can be 4*760c253cSXin Li# found in the LICENSE file. 5*760c253cSXin Li 6*760c253cSXin Li"""Performs bisection on LLVM based off a .JSON file.""" 7*760c253cSXin Li 8*760c253cSXin Liimport argparse 9*760c253cSXin Liimport enum 10*760c253cSXin Liimport errno 11*760c253cSXin Liimport json 12*760c253cSXin Liimport os 13*760c253cSXin Liimport subprocess 14*760c253cSXin Liimport sys 15*760c253cSXin Li 16*760c253cSXin Liimport chroot 17*760c253cSXin Liimport get_llvm_hash 18*760c253cSXin Liimport git_llvm_rev 19*760c253cSXin Liimport modify_a_tryjob 20*760c253cSXin Liimport update_chromeos_llvm_hash 21*760c253cSXin Liimport update_tryjob_status 22*760c253cSXin Li 23*760c253cSXin Li 24*760c253cSXin Liclass BisectionExitStatus(enum.Enum): 25*760c253cSXin Li """Exit code when performing bisection.""" 26*760c253cSXin Li 27*760c253cSXin Li # Means that there are no more revisions available to bisect. 28*760c253cSXin Li BISECTION_COMPLETE = 126 29*760c253cSXin Li 30*760c253cSXin Li 31*760c253cSXin Lidef GetCommandLineArgs(): 32*760c253cSXin Li """Parses the command line for the command line arguments.""" 33*760c253cSXin Li 34*760c253cSXin Li # Default path to the chroot if a path is not specified. 35*760c253cSXin Li cros_root = os.path.expanduser("~") 36*760c253cSXin Li cros_root = os.path.join(cros_root, "chromiumos") 37*760c253cSXin Li 38*760c253cSXin Li # Create parser and add optional command-line arguments. 39*760c253cSXin Li parser = argparse.ArgumentParser( 40*760c253cSXin Li description="Bisects LLVM via tracking a JSON file." 41*760c253cSXin Li ) 42*760c253cSXin Li 43*760c253cSXin Li # Add argument for other change lists that want to run alongside the tryjob 44*760c253cSXin Li # which has a change list of updating a package's git hash. 45*760c253cSXin Li parser.add_argument( 46*760c253cSXin Li "--parallel", 47*760c253cSXin Li type=int, 48*760c253cSXin Li default=3, 49*760c253cSXin Li help="How many tryjobs to create between the last good version and " 50*760c253cSXin Li "the first bad version (default: %(default)s)", 51*760c253cSXin Li ) 52*760c253cSXin Li 53*760c253cSXin Li # Add argument for the good LLVM revision for bisection. 54*760c253cSXin Li parser.add_argument( 55*760c253cSXin Li "--start_rev", 56*760c253cSXin Li required=True, 57*760c253cSXin Li type=int, 58*760c253cSXin Li help="The good revision for the bisection.", 59*760c253cSXin Li ) 60*760c253cSXin Li 61*760c253cSXin Li # Add argument for the bad LLVM revision for bisection. 62*760c253cSXin Li parser.add_argument( 63*760c253cSXin Li "--end_rev", 64*760c253cSXin Li required=True, 65*760c253cSXin Li type=int, 66*760c253cSXin Li help="The bad revision for the bisection.", 67*760c253cSXin Li ) 68*760c253cSXin Li 69*760c253cSXin Li # Add argument for the absolute path to the file that contains information 70*760c253cSXin Li # on the previous tested svn version. 71*760c253cSXin Li parser.add_argument( 72*760c253cSXin Li "--last_tested", 73*760c253cSXin Li required=True, 74*760c253cSXin Li help="the absolute path to the file that contains the tryjobs", 75*760c253cSXin Li ) 76*760c253cSXin Li 77*760c253cSXin Li # Add argument for the absolute path to the LLVM source tree. 78*760c253cSXin Li parser.add_argument( 79*760c253cSXin Li "--src_path", 80*760c253cSXin Li help="the path to the LLVM source tree to use (used for retrieving the " 81*760c253cSXin Li "git hash of each version between the last good version and first bad " 82*760c253cSXin Li "version)", 83*760c253cSXin Li ) 84*760c253cSXin Li 85*760c253cSXin Li # Add argument for other change lists that want to run alongside the tryjob 86*760c253cSXin Li # which has a change list of updating a package's git hash. 87*760c253cSXin Li parser.add_argument( 88*760c253cSXin Li "--extra_change_lists", 89*760c253cSXin Li type=int, 90*760c253cSXin Li nargs="+", 91*760c253cSXin Li help="change lists that would like to be run alongside the change list " 92*760c253cSXin Li "of updating the packages", 93*760c253cSXin Li ) 94*760c253cSXin Li 95*760c253cSXin Li # Add argument for custom options for the tryjob. 96*760c253cSXin Li parser.add_argument( 97*760c253cSXin Li "--options", 98*760c253cSXin Li required=False, 99*760c253cSXin Li nargs="+", 100*760c253cSXin Li help="options to use for the tryjob testing", 101*760c253cSXin Li ) 102*760c253cSXin Li 103*760c253cSXin Li # Add argument for the builder to use for the tryjob. 104*760c253cSXin Li parser.add_argument( 105*760c253cSXin Li "--builder", required=True, help="builder to use for the tryjob testing" 106*760c253cSXin Li ) 107*760c253cSXin Li 108*760c253cSXin Li # Add argument for the description of the tryjob. 109*760c253cSXin Li parser.add_argument( 110*760c253cSXin Li "--description", 111*760c253cSXin Li required=False, 112*760c253cSXin Li nargs="+", 113*760c253cSXin Li help="the description of the tryjob", 114*760c253cSXin Li ) 115*760c253cSXin Li 116*760c253cSXin Li # Add argument for a specific chroot path. 117*760c253cSXin Li parser.add_argument( 118*760c253cSXin Li "--chromeos_path", 119*760c253cSXin Li default=cros_root, 120*760c253cSXin Li help="the path to the chroot (default: %(default)s)", 121*760c253cSXin Li ) 122*760c253cSXin Li 123*760c253cSXin Li # Add argument for whether to display command contents to `stdout`. 124*760c253cSXin Li parser.add_argument( 125*760c253cSXin Li "--nocleanup", 126*760c253cSXin Li action="store_false", 127*760c253cSXin Li dest="cleanup", 128*760c253cSXin Li help="Abandon CLs created for bisectoin", 129*760c253cSXin Li ) 130*760c253cSXin Li 131*760c253cSXin Li args_output = parser.parse_args() 132*760c253cSXin Li 133*760c253cSXin Li assert ( 134*760c253cSXin Li args_output.start_rev < args_output.end_rev 135*760c253cSXin Li ), "Start revision %d is >= end revision %d" % ( 136*760c253cSXin Li args_output.start_rev, 137*760c253cSXin Li args_output.end_rev, 138*760c253cSXin Li ) 139*760c253cSXin Li 140*760c253cSXin Li if args_output.last_tested and not args_output.last_tested.endswith( 141*760c253cSXin Li ".json" 142*760c253cSXin Li ): 143*760c253cSXin Li raise ValueError( 144*760c253cSXin Li 'Filed provided %s does not end in ".json"' 145*760c253cSXin Li % args_output.last_tested 146*760c253cSXin Li ) 147*760c253cSXin Li 148*760c253cSXin Li return args_output 149*760c253cSXin Li 150*760c253cSXin Li 151*760c253cSXin Lidef GetRemainingRange(start, end, tryjobs): 152*760c253cSXin Li """Gets the start and end intervals in 'json_file'. 153*760c253cSXin Li 154*760c253cSXin Li Args: 155*760c253cSXin Li start: The start version of the bisection provided via the command line. 156*760c253cSXin Li end: The end version of the bisection provided via the command line. 157*760c253cSXin Li tryjobs: A list of tryjobs where each element is in the following 158*760c253cSXin Li format: 159*760c253cSXin Li [ 160*760c253cSXin Li {[TRYJOB_INFORMATION]}, 161*760c253cSXin Li {[TRYJOB_INFORMATION]}, 162*760c253cSXin Li ..., 163*760c253cSXin Li {[TRYJOB_INFORMATION]} 164*760c253cSXin Li ] 165*760c253cSXin Li 166*760c253cSXin Li Returns: 167*760c253cSXin Li The new start version and end version for bisection, a set of revisions 168*760c253cSXin Li that are 'pending' and a set of revisions that are to be skipped. 169*760c253cSXin Li 170*760c253cSXin Li Raises: 171*760c253cSXin Li ValueError: The value for 'status' is missing or there is a mismatch 172*760c253cSXin Li between 'start' and 'end' compared to the 'start' and 'end' in the JSON 173*760c253cSXin Li file. 174*760c253cSXin Li AssertionError: The new start version is >= than the new end version. 175*760c253cSXin Li """ 176*760c253cSXin Li 177*760c253cSXin Li if not tryjobs: 178*760c253cSXin Li return start, end, {}, {} 179*760c253cSXin Li 180*760c253cSXin Li # Verify that each tryjob has a value for the 'status' key. 181*760c253cSXin Li for cur_tryjob_dict in tryjobs: 182*760c253cSXin Li if not cur_tryjob_dict.get("status", None): 183*760c253cSXin Li raise ValueError( 184*760c253cSXin Li '"status" is missing or has no value, please ' 185*760c253cSXin Li "go to %s and update it" % cur_tryjob_dict["link"] 186*760c253cSXin Li ) 187*760c253cSXin Li 188*760c253cSXin Li all_bad_revisions = [end] 189*760c253cSXin Li all_bad_revisions.extend( 190*760c253cSXin Li cur_tryjob["rev"] 191*760c253cSXin Li for cur_tryjob in tryjobs 192*760c253cSXin Li if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.BAD.value 193*760c253cSXin Li ) 194*760c253cSXin Li 195*760c253cSXin Li # The minimum value for the 'bad' field in the tryjobs is the new end 196*760c253cSXin Li # version. 197*760c253cSXin Li bad_rev = min(all_bad_revisions) 198*760c253cSXin Li 199*760c253cSXin Li all_good_revisions = [start] 200*760c253cSXin Li all_good_revisions.extend( 201*760c253cSXin Li cur_tryjob["rev"] 202*760c253cSXin Li for cur_tryjob in tryjobs 203*760c253cSXin Li if cur_tryjob["status"] == update_tryjob_status.TryjobStatus.GOOD.value 204*760c253cSXin Li ) 205*760c253cSXin Li 206*760c253cSXin Li # The maximum value for the 'good' field in the tryjobs is the new start 207*760c253cSXin Li # version. 208*760c253cSXin Li good_rev = max(all_good_revisions) 209*760c253cSXin Li 210*760c253cSXin Li # The good version should always be strictly less than the bad version; 211*760c253cSXin Li # otherwise, bisection is broken. 212*760c253cSXin Li assert ( 213*760c253cSXin Li good_rev < bad_rev 214*760c253cSXin Li ), "Bisection is broken because %d (good) is >= " "%d (bad)" % ( 215*760c253cSXin Li good_rev, 216*760c253cSXin Li bad_rev, 217*760c253cSXin Li ) 218*760c253cSXin Li 219*760c253cSXin Li # Find all revisions that are 'pending' within 'good_rev' and 'bad_rev'. 220*760c253cSXin Li # 221*760c253cSXin Li # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev' 222*760c253cSXin Li # that have already been launched (this set is used when constructing the 223*760c253cSXin Li # list of revisions to launch tryjobs for). 224*760c253cSXin Li pending_revisions = { 225*760c253cSXin Li tryjob["rev"] 226*760c253cSXin Li for tryjob in tryjobs 227*760c253cSXin Li if tryjob["status"] == update_tryjob_status.TryjobStatus.PENDING.value 228*760c253cSXin Li and good_rev < tryjob["rev"] < bad_rev 229*760c253cSXin Li } 230*760c253cSXin Li 231*760c253cSXin Li # Find all revisions that are to be skipped within 'good_rev' and 'bad_rev'. 232*760c253cSXin Li # 233*760c253cSXin Li # NOTE: The intent is to not launch tryjobs between 'good_rev' and 'bad_rev' 234*760c253cSXin Li # that have already been marked as 'skip' (this set is used when 235*760c253cSXin Li # constructing the list of revisions to launch tryjobs for). 236*760c253cSXin Li skip_revisions = { 237*760c253cSXin Li tryjob["rev"] 238*760c253cSXin Li for tryjob in tryjobs 239*760c253cSXin Li if tryjob["status"] == update_tryjob_status.TryjobStatus.SKIP.value 240*760c253cSXin Li and good_rev < tryjob["rev"] < bad_rev 241*760c253cSXin Li } 242*760c253cSXin Li 243*760c253cSXin Li return good_rev, bad_rev, pending_revisions, skip_revisions 244*760c253cSXin Li 245*760c253cSXin Li 246*760c253cSXin Lidef GetCommitsBetween( 247*760c253cSXin Li start, end, parallel, src_path, pending_revisions, skip_revisions 248*760c253cSXin Li): 249*760c253cSXin Li """Determines the revisions between start and end.""" 250*760c253cSXin Li 251*760c253cSXin Li with get_llvm_hash.LLVMHash().CreateTempDirectory() as temp_dir: 252*760c253cSXin Li # We have guaranteed contiguous revision numbers after this, 253*760c253cSXin Li # and that guarnatee simplifies things considerably, so we don't 254*760c253cSXin Li # support anything before it. 255*760c253cSXin Li assert ( 256*760c253cSXin Li start >= git_llvm_rev.base_llvm_revision 257*760c253cSXin Li ), f"{start} was too long ago" 258*760c253cSXin Li 259*760c253cSXin Li with get_llvm_hash.CreateTempLLVMRepo(temp_dir) as new_repo: 260*760c253cSXin Li if not src_path: 261*760c253cSXin Li src_path = new_repo 262*760c253cSXin Li index_step = (end - (start + 1)) // (parallel + 1) 263*760c253cSXin Li if not index_step: 264*760c253cSXin Li index_step = 1 265*760c253cSXin Li revisions = [ 266*760c253cSXin Li rev 267*760c253cSXin Li for rev in range(start + 1, end, index_step) 268*760c253cSXin Li if rev not in pending_revisions and rev not in skip_revisions 269*760c253cSXin Li ] 270*760c253cSXin Li git_hashes = [ 271*760c253cSXin Li get_llvm_hash.GetGitHashFrom(src_path, rev) for rev in revisions 272*760c253cSXin Li ] 273*760c253cSXin Li return revisions, git_hashes 274*760c253cSXin Li 275*760c253cSXin Li 276*760c253cSXin Lidef Bisect( 277*760c253cSXin Li revisions, 278*760c253cSXin Li git_hashes, 279*760c253cSXin Li bisect_state, 280*760c253cSXin Li last_tested, 281*760c253cSXin Li update_packages, 282*760c253cSXin Li chromeos_path, 283*760c253cSXin Li extra_change_lists, 284*760c253cSXin Li options, 285*760c253cSXin Li builder, 286*760c253cSXin Li): 287*760c253cSXin Li """Adds tryjobs and updates the status file with the new tryjobs.""" 288*760c253cSXin Li 289*760c253cSXin Li try: 290*760c253cSXin Li for svn_revision, git_hash in zip(revisions, git_hashes): 291*760c253cSXin Li tryjob_dict = modify_a_tryjob.AddTryjob( 292*760c253cSXin Li update_packages, 293*760c253cSXin Li git_hash, 294*760c253cSXin Li svn_revision, 295*760c253cSXin Li chromeos_path, 296*760c253cSXin Li extra_change_lists, 297*760c253cSXin Li options, 298*760c253cSXin Li builder, 299*760c253cSXin Li svn_revision, 300*760c253cSXin Li ) 301*760c253cSXin Li 302*760c253cSXin Li bisect_state["jobs"].append(tryjob_dict) 303*760c253cSXin Li finally: 304*760c253cSXin Li # Do not want to lose progress if there is an exception. 305*760c253cSXin Li if last_tested: 306*760c253cSXin Li new_file = "%s.new" % last_tested 307*760c253cSXin Li with open(new_file, "w", encoding="utf-8") as json_file: 308*760c253cSXin Li json.dump( 309*760c253cSXin Li bisect_state, json_file, indent=4, separators=(",", ": ") 310*760c253cSXin Li ) 311*760c253cSXin Li 312*760c253cSXin Li os.rename(new_file, last_tested) 313*760c253cSXin Li 314*760c253cSXin Li 315*760c253cSXin Lidef LoadStatusFile(last_tested, start, end): 316*760c253cSXin Li """Loads the status file for bisection.""" 317*760c253cSXin Li 318*760c253cSXin Li try: 319*760c253cSXin Li with open(last_tested, encoding="utf-8") as f: 320*760c253cSXin Li return json.load(f) 321*760c253cSXin Li except IOError as err: 322*760c253cSXin Li if err.errno != errno.ENOENT: 323*760c253cSXin Li raise 324*760c253cSXin Li 325*760c253cSXin Li return {"start": start, "end": end, "jobs": []} 326*760c253cSXin Li 327*760c253cSXin Li 328*760c253cSXin Lidef main(args_output): 329*760c253cSXin Li """Bisects LLVM commits. 330*760c253cSXin Li 331*760c253cSXin Li Raises: 332*760c253cSXin Li AssertionError: The script was run inside the chroot. 333*760c253cSXin Li """ 334*760c253cSXin Li 335*760c253cSXin Li chroot.VerifyOutsideChroot() 336*760c253cSXin Li chroot.VerifyChromeOSRoot(args_output.chromeos_path) 337*760c253cSXin Li start = args_output.start_rev 338*760c253cSXin Li end = args_output.end_rev 339*760c253cSXin Li 340*760c253cSXin Li bisect_state = LoadStatusFile(args_output.last_tested, start, end) 341*760c253cSXin Li if start != bisect_state["start"] or end != bisect_state["end"]: 342*760c253cSXin Li raise ValueError( 343*760c253cSXin Li f"The start {start} or the end {end} version provided is " 344*760c253cSXin Li f'different than "start" {bisect_state["start"]} or "end" ' 345*760c253cSXin Li f'{bisect_state["end"]} in the .JSON file' 346*760c253cSXin Li ) 347*760c253cSXin Li 348*760c253cSXin Li # Pending and skipped revisions are between 'start_rev' and 'end_rev'. 349*760c253cSXin Li start_rev, end_rev, pending_revs, skip_revs = GetRemainingRange( 350*760c253cSXin Li start, end, bisect_state["jobs"] 351*760c253cSXin Li ) 352*760c253cSXin Li 353*760c253cSXin Li revisions, git_hashes = GetCommitsBetween( 354*760c253cSXin Li start_rev, 355*760c253cSXin Li end_rev, 356*760c253cSXin Li args_output.parallel, 357*760c253cSXin Li args_output.src_path, 358*760c253cSXin Li pending_revs, 359*760c253cSXin Li skip_revs, 360*760c253cSXin Li ) 361*760c253cSXin Li 362*760c253cSXin Li # No more revisions between 'start_rev' and 'end_rev', so 363*760c253cSXin Li # bisection is complete. 364*760c253cSXin Li # 365*760c253cSXin Li # This is determined by finding all valid revisions between 'start_rev' 366*760c253cSXin Li # and 'end_rev' and that are NOT in the 'pending' and 'skipped' set. 367*760c253cSXin Li if not revisions: 368*760c253cSXin Li if pending_revs: 369*760c253cSXin Li # Some tryjobs are not finished which may change the actual bad 370*760c253cSXin Li # commit/revision when those tryjobs are finished. 371*760c253cSXin Li no_revisions_message = ( 372*760c253cSXin Li f"No revisions between start {start_rev} " 373*760c253cSXin Li f"and end {end_rev} to create tryjobs\n" 374*760c253cSXin Li ) 375*760c253cSXin Li 376*760c253cSXin Li if pending_revs: 377*760c253cSXin Li no_revisions_message += ( 378*760c253cSXin Li "The following tryjobs are pending:\n" 379*760c253cSXin Li + "\n".join(str(rev) for rev in pending_revs) 380*760c253cSXin Li + "\n" 381*760c253cSXin Li ) 382*760c253cSXin Li 383*760c253cSXin Li if skip_revs: 384*760c253cSXin Li no_revisions_message += ( 385*760c253cSXin Li "The following tryjobs were skipped:\n" 386*760c253cSXin Li + "\n".join(str(rev) for rev in skip_revs) 387*760c253cSXin Li + "\n" 388*760c253cSXin Li ) 389*760c253cSXin Li 390*760c253cSXin Li raise ValueError(no_revisions_message) 391*760c253cSXin Li 392*760c253cSXin Li print(f"Finished bisecting for {args_output.last_tested}") 393*760c253cSXin Li if args_output.src_path: 394*760c253cSXin Li bad_llvm_hash = get_llvm_hash.GetGitHashFrom( 395*760c253cSXin Li args_output.src_path, end_rev 396*760c253cSXin Li ) 397*760c253cSXin Li else: 398*760c253cSXin Li bad_llvm_hash = get_llvm_hash.LLVMHash().GetLLVMHash(end_rev) 399*760c253cSXin Li print( 400*760c253cSXin Li f"The bad revision is {end_rev} and its commit hash is " 401*760c253cSXin Li f"{bad_llvm_hash}" 402*760c253cSXin Li ) 403*760c253cSXin Li if skip_revs: 404*760c253cSXin Li skip_revs_message = ( 405*760c253cSXin Li "\nThe following revisions were skipped:\n" 406*760c253cSXin Li + "\n".join(str(rev) for rev in skip_revs) 407*760c253cSXin Li ) 408*760c253cSXin Li print(skip_revs_message) 409*760c253cSXin Li 410*760c253cSXin Li if args_output.cleanup: 411*760c253cSXin Li # Abandon all the CLs created for bisection 412*760c253cSXin Li gerrit = os.path.join( 413*760c253cSXin Li args_output.chromeos_path, "chromite/bin/gerrit" 414*760c253cSXin Li ) 415*760c253cSXin Li for build in bisect_state["jobs"]: 416*760c253cSXin Li try: 417*760c253cSXin Li subprocess.check_output( 418*760c253cSXin Li [gerrit, "abandon", str(build["cl"])], 419*760c253cSXin Li stderr=subprocess.STDOUT, 420*760c253cSXin Li encoding="utf-8", 421*760c253cSXin Li ) 422*760c253cSXin Li except subprocess.CalledProcessError as err: 423*760c253cSXin Li # the CL may have been abandoned 424*760c253cSXin Li if "chromite.lib.gob_util.GOBError" not in err.output: 425*760c253cSXin Li raise 426*760c253cSXin Li 427*760c253cSXin Li return BisectionExitStatus.BISECTION_COMPLETE.value 428*760c253cSXin Li 429*760c253cSXin Li for rev in revisions: 430*760c253cSXin Li if ( 431*760c253cSXin Li update_tryjob_status.FindTryjobIndex(rev, bisect_state["jobs"]) 432*760c253cSXin Li is not None 433*760c253cSXin Li ): 434*760c253cSXin Li raise ValueError(f'Revision {rev} exists already in "jobs"') 435*760c253cSXin Li 436*760c253cSXin Li Bisect( 437*760c253cSXin Li revisions, 438*760c253cSXin Li git_hashes, 439*760c253cSXin Li bisect_state, 440*760c253cSXin Li args_output.last_tested, 441*760c253cSXin Li update_chromeos_llvm_hash.DEFAULT_PACKAGES, 442*760c253cSXin Li args_output.chromeos_path, 443*760c253cSXin Li args_output.extra_change_lists, 444*760c253cSXin Li args_output.options, 445*760c253cSXin Li args_output.builder, 446*760c253cSXin Li ) 447*760c253cSXin Li 448*760c253cSXin Li 449*760c253cSXin Liif __name__ == "__main__": 450*760c253cSXin Li sys.exit(main(GetCommandLineArgs())) 451