1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2024 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"""Simple LLVM Bisection Script for use with the llvm-9999 ebuild. 7*760c253cSXin Li 8*760c253cSXin LiExample usage with `git bisect`: 9*760c253cSXin Li 10*760c253cSXin Li cd path/to/llvm-project 11*760c253cSXin Li git bisect good <GOOD_HASH> 12*760c253cSXin Li git bisect bad <BAD_HASH> 13*760c253cSXin Li git bisect run \ 14*760c253cSXin Li path/to/llvm_tools/llvm_simple_bisect.py --reset-llvm \ 15*760c253cSXin Li --test "emerge-atlas package" \ 16*760c253cSXin Li --search-error "some error that I care about" 17*760c253cSXin Li""" 18*760c253cSXin Li 19*760c253cSXin Liimport argparse 20*760c253cSXin Liimport dataclasses 21*760c253cSXin Liimport logging 22*760c253cSXin Liimport os 23*760c253cSXin Lifrom pathlib import Path 24*760c253cSXin Liimport subprocess 25*760c253cSXin Liimport sys 26*760c253cSXin Lifrom typing import Optional, Text 27*760c253cSXin Li 28*760c253cSXin Liimport chroot 29*760c253cSXin Li 30*760c253cSXin Li 31*760c253cSXin Li# Git Bisection exit codes 32*760c253cSXin LiEXIT_GOOD = 0 33*760c253cSXin LiEXIT_BAD = 1 34*760c253cSXin LiEXIT_SKIP = 125 35*760c253cSXin LiEXIT_ABORT = 255 36*760c253cSXin Li 37*760c253cSXin Li 38*760c253cSXin Liclass AbortingException(Exception): 39*760c253cSXin Li """A nonrecoverable error occurred which should not depend on the LLVM Hash. 40*760c253cSXin Li 41*760c253cSXin Li In this case we will abort bisection unless --never-abort is set. 42*760c253cSXin Li """ 43*760c253cSXin Li 44*760c253cSXin Li 45*760c253cSXin Li@dataclasses.dataclass(frozen=True) 46*760c253cSXin Liclass CommandResult: 47*760c253cSXin Li """Results a command""" 48*760c253cSXin Li 49*760c253cSXin Li return_code: int 50*760c253cSXin Li output: Text 51*760c253cSXin Li 52*760c253cSXin Li def success(self) -> bool: 53*760c253cSXin Li """Checks if command exited successfully.""" 54*760c253cSXin Li return self.return_code == 0 55*760c253cSXin Li 56*760c253cSXin Li def search(self, error_string: Text) -> bool: 57*760c253cSXin Li """Checks if command has error_string in output.""" 58*760c253cSXin Li return error_string in self.output 59*760c253cSXin Li 60*760c253cSXin Li def exit_assert( 61*760c253cSXin Li self, 62*760c253cSXin Li error_string: Text, 63*760c253cSXin Li llvm_hash: Text, 64*760c253cSXin Li log_dir: Optional[Path] = None, 65*760c253cSXin Li ): 66*760c253cSXin Li """Exit program with error code based on result.""" 67*760c253cSXin Li if self.success(): 68*760c253cSXin Li decision, decision_str = EXIT_GOOD, "GOOD" 69*760c253cSXin Li elif self.search(error_string): 70*760c253cSXin Li if error_string: 71*760c253cSXin Li logging.info("Found failure and output contained error_string") 72*760c253cSXin Li decision, decision_str = EXIT_BAD, "BAD" 73*760c253cSXin Li else: 74*760c253cSXin Li if error_string: 75*760c253cSXin Li logging.info( 76*760c253cSXin Li "Found failure but error_string was not found in results." 77*760c253cSXin Li ) 78*760c253cSXin Li decision, decision_str = EXIT_SKIP, "SKIP" 79*760c253cSXin Li 80*760c253cSXin Li logging.info("Completed bisection stage with: %s", decision_str) 81*760c253cSXin Li if log_dir: 82*760c253cSXin Li self.log_result(log_dir, llvm_hash, decision_str) 83*760c253cSXin Li sys.exit(decision) 84*760c253cSXin Li 85*760c253cSXin Li def log_result(self, log_dir: Path, llvm_hash: Text, decision: Text): 86*760c253cSXin Li """Log command's output to `{log_dir}/{llvm_hash}.{decision}`. 87*760c253cSXin Li 88*760c253cSXin Li Args: 89*760c253cSXin Li log_dir: Path to the directory to use for log files 90*760c253cSXin Li llvm_hash: LLVM Hash being tested 91*760c253cSXin Li decision: GOOD, BAD, or SKIP decision returned for `git bisect` 92*760c253cSXin Li """ 93*760c253cSXin Li log_dir = Path(log_dir) 94*760c253cSXin Li log_dir.mkdir(parents=True, exist_ok=True) 95*760c253cSXin Li 96*760c253cSXin Li log_file = log_dir / f"{llvm_hash}.{decision}" 97*760c253cSXin Li log_file.touch() 98*760c253cSXin Li 99*760c253cSXin Li logging.info("Writing output logs to %s", log_file) 100*760c253cSXin Li 101*760c253cSXin Li log_file.write_text(self.output, encoding="utf-8") 102*760c253cSXin Li 103*760c253cSXin Li # Fix permissions since sometimes this script gets called with sudo 104*760c253cSXin Li log_dir.chmod(0o666) 105*760c253cSXin Li log_file.chmod(0o666) 106*760c253cSXin Li 107*760c253cSXin Li 108*760c253cSXin Liclass LLVMRepo: 109*760c253cSXin Li """LLVM Repository git and workon information.""" 110*760c253cSXin Li 111*760c253cSXin Li REPO_PATH = Path("/mnt/host/source/src/third_party/llvm-project") 112*760c253cSXin Li 113*760c253cSXin Li def __init__(self): 114*760c253cSXin Li self.workon: Optional[bool] = None 115*760c253cSXin Li 116*760c253cSXin Li def get_current_hash(self) -> Text: 117*760c253cSXin Li try: 118*760c253cSXin Li output = subprocess.check_output( 119*760c253cSXin Li ["git", "rev-parse", "HEAD"], 120*760c253cSXin Li cwd=self.REPO_PATH, 121*760c253cSXin Li encoding="utf-8", 122*760c253cSXin Li ) 123*760c253cSXin Li output = output.strip() 124*760c253cSXin Li except subprocess.CalledProcessError as e: 125*760c253cSXin Li output = e.output 126*760c253cSXin Li logging.error("Could not get current llvm hash") 127*760c253cSXin Li raise AbortingException 128*760c253cSXin Li return output 129*760c253cSXin Li 130*760c253cSXin Li def set_workon(self, workon: bool): 131*760c253cSXin Li """Toggle llvm-9999 mode on or off.""" 132*760c253cSXin Li if self.workon == workon: 133*760c253cSXin Li return 134*760c253cSXin Li subcommand = "start" if workon else "stop" 135*760c253cSXin Li try: 136*760c253cSXin Li subprocess.check_call( 137*760c253cSXin Li ["cros_workon", "--host", subcommand, "sys-devel/llvm"] 138*760c253cSXin Li ) 139*760c253cSXin Li except subprocess.CalledProcessError: 140*760c253cSXin Li logging.exception("cros_workon could not be toggled for LLVM.") 141*760c253cSXin Li raise AbortingException 142*760c253cSXin Li self.workon = workon 143*760c253cSXin Li 144*760c253cSXin Li def reset(self): 145*760c253cSXin Li """Reset installed LLVM version.""" 146*760c253cSXin Li logging.info("Reseting llvm to downloaded binary.") 147*760c253cSXin Li self.set_workon(False) 148*760c253cSXin Li files_to_rm = Path("/var/lib/portage/pkgs").glob("sys-*/*") 149*760c253cSXin Li try: 150*760c253cSXin Li subprocess.check_call( 151*760c253cSXin Li ["sudo", "rm", "-f"] + [str(f) for f in files_to_rm] 152*760c253cSXin Li ) 153*760c253cSXin Li subprocess.check_call(["emerge", "-C", "llvm"]) 154*760c253cSXin Li subprocess.check_call(["emerge", "-G", "llvm"]) 155*760c253cSXin Li except subprocess.CalledProcessError: 156*760c253cSXin Li logging.exception("LLVM could not be reset.") 157*760c253cSXin Li raise AbortingException 158*760c253cSXin Li 159*760c253cSXin Li def build(self, use_flags: Text) -> CommandResult: 160*760c253cSXin Li """Build selected LLVM version.""" 161*760c253cSXin Li logging.info( 162*760c253cSXin Li "Building llvm with candidate hash. Use flags will be %s", use_flags 163*760c253cSXin Li ) 164*760c253cSXin Li self.set_workon(True) 165*760c253cSXin Li try: 166*760c253cSXin Li output = subprocess.check_output( 167*760c253cSXin Li ["sudo", "emerge", "llvm"], 168*760c253cSXin Li env={"USE": use_flags, **os.environ}, 169*760c253cSXin Li encoding="utf-8", 170*760c253cSXin Li stderr=subprocess.STDOUT, 171*760c253cSXin Li ) 172*760c253cSXin Li return_code = 0 173*760c253cSXin Li except subprocess.CalledProcessError as e: 174*760c253cSXin Li return_code = e.returncode 175*760c253cSXin Li output = e.output 176*760c253cSXin Li return CommandResult(return_code, output) 177*760c253cSXin Li 178*760c253cSXin Li 179*760c253cSXin Lidef run_test(command: Text) -> CommandResult: 180*760c253cSXin Li """Run test command and get a CommandResult.""" 181*760c253cSXin Li logging.info("Running test command: %s", command) 182*760c253cSXin Li result = subprocess.run( 183*760c253cSXin Li command, 184*760c253cSXin Li check=False, 185*760c253cSXin Li encoding="utf-8", 186*760c253cSXin Li shell=True, 187*760c253cSXin Li stdout=subprocess.PIPE, 188*760c253cSXin Li stderr=subprocess.STDOUT, 189*760c253cSXin Li ) 190*760c253cSXin Li logging.info("Test command returned with: %d", result.returncode) 191*760c253cSXin Li return CommandResult(result.returncode, result.stdout) 192*760c253cSXin Li 193*760c253cSXin Li 194*760c253cSXin Lidef get_use_flags( 195*760c253cSXin Li use_debug: bool, use_lto: bool, error_on_patch_failure: bool 196*760c253cSXin Li) -> str: 197*760c253cSXin Li """Get the USE flags for building LLVM.""" 198*760c253cSXin Li use_flags = [] 199*760c253cSXin Li if use_debug: 200*760c253cSXin Li use_flags.append("debug") 201*760c253cSXin Li if not use_lto: 202*760c253cSXin Li use_flags.append("-thinlto") 203*760c253cSXin Li use_flags.append("-llvm_pgo_use") 204*760c253cSXin Li if not error_on_patch_failure: 205*760c253cSXin Li use_flags.append("continue-on-patch-failure") 206*760c253cSXin Li return " ".join(use_flags) 207*760c253cSXin Li 208*760c253cSXin Li 209*760c253cSXin Lidef abort(never_abort: bool): 210*760c253cSXin Li """Exit with EXIT_ABORT or else EXIT_SKIP if never_abort is set.""" 211*760c253cSXin Li if never_abort: 212*760c253cSXin Li logging.info( 213*760c253cSXin Li "Would have aborted but --never-abort was set. " 214*760c253cSXin Li "Completed bisection stage with: SKIP" 215*760c253cSXin Li ) 216*760c253cSXin Li sys.exit(EXIT_SKIP) 217*760c253cSXin Li else: 218*760c253cSXin Li logging.info("Completed bisection stage with: ABORT") 219*760c253cSXin Li sys.exit(EXIT_ABORT) 220*760c253cSXin Li 221*760c253cSXin Li 222*760c253cSXin Lidef get_args() -> argparse.Namespace: 223*760c253cSXin Li parser = argparse.ArgumentParser( 224*760c253cSXin Li description="Simple LLVM Bisection Script for use with llvm-9999." 225*760c253cSXin Li ) 226*760c253cSXin Li 227*760c253cSXin Li parser.add_argument( 228*760c253cSXin Li "--never-abort", 229*760c253cSXin Li action="store_true", 230*760c253cSXin Li help=( 231*760c253cSXin Li "Return SKIP (125) for unrecoverable hash-independent errors " 232*760c253cSXin Li "instead of ABORT (255)." 233*760c253cSXin Li ), 234*760c253cSXin Li ) 235*760c253cSXin Li parser.add_argument( 236*760c253cSXin Li "--reset-llvm", 237*760c253cSXin Li action="store_true", 238*760c253cSXin Li help="Reset llvm with downloaded prebuilds before rebuilding", 239*760c253cSXin Li ) 240*760c253cSXin Li parser.add_argument( 241*760c253cSXin Li "--skip-build", 242*760c253cSXin Li action="store_true", 243*760c253cSXin Li help="Don't build or reset llvm, even if --reset-llvm is set.", 244*760c253cSXin Li ) 245*760c253cSXin Li parser.add_argument( 246*760c253cSXin Li "--use-debug", 247*760c253cSXin Li action="store_true", 248*760c253cSXin Li help="Build llvm with assertions enabled", 249*760c253cSXin Li ) 250*760c253cSXin Li parser.add_argument( 251*760c253cSXin Li "--use-lto", 252*760c253cSXin Li action="store_true", 253*760c253cSXin Li help="Build llvm with thinlto and PGO. This will increase build times.", 254*760c253cSXin Li ) 255*760c253cSXin Li parser.add_argument( 256*760c253cSXin Li "--error-on-patch-failure", 257*760c253cSXin Li action="store_true", 258*760c253cSXin Li help="Don't add continue-on-patch-failure to LLVM use flags.", 259*760c253cSXin Li ) 260*760c253cSXin Li 261*760c253cSXin Li test_group = parser.add_mutually_exclusive_group(required=True) 262*760c253cSXin Li test_group.add_argument( 263*760c253cSXin Li "--test-llvm-build", 264*760c253cSXin Li action="store_true", 265*760c253cSXin Li help="Bisect the llvm build instead of a test command/script.", 266*760c253cSXin Li ) 267*760c253cSXin Li test_group.add_argument( 268*760c253cSXin Li "--test", help="Command to test (exp. 'emerge-atlas grpc')" 269*760c253cSXin Li ) 270*760c253cSXin Li 271*760c253cSXin Li parser.add_argument( 272*760c253cSXin Li "--search-error", 273*760c253cSXin Li default="", 274*760c253cSXin Li help=( 275*760c253cSXin Li "Search for an error string from test if test has nonzero exit " 276*760c253cSXin Li "code. If test has a non-zero exit code but search string is not " 277*760c253cSXin Li "found, git bisect SKIP will be used." 278*760c253cSXin Li ), 279*760c253cSXin Li ) 280*760c253cSXin Li parser.add_argument( 281*760c253cSXin Li "--log-dir", 282*760c253cSXin Li help=( 283*760c253cSXin Li "Save a log of each output to a directory. " 284*760c253cSXin Li "Logs will be written to `{log_dir}/{llvm_hash}.{decision}`" 285*760c253cSXin Li ), 286*760c253cSXin Li ) 287*760c253cSXin Li 288*760c253cSXin Li return parser.parse_args() 289*760c253cSXin Li 290*760c253cSXin Li 291*760c253cSXin Lidef run(opts: argparse.Namespace): 292*760c253cSXin Li # Validate path to Log dir. 293*760c253cSXin Li log_dir = opts.log_dir 294*760c253cSXin Li if log_dir: 295*760c253cSXin Li log_dir = Path(log_dir) 296*760c253cSXin Li if log_dir.exists() and not log_dir.is_dir(): 297*760c253cSXin Li logging.error("argument --log-dir: Given path is not a directory!") 298*760c253cSXin Li raise AbortingException() 299*760c253cSXin Li 300*760c253cSXin Li # Get LLVM repo 301*760c253cSXin Li llvm_repo = LLVMRepo() 302*760c253cSXin Li llvm_hash = llvm_repo.get_current_hash() 303*760c253cSXin Li logging.info("Testing LLVM Hash: %s", llvm_hash) 304*760c253cSXin Li 305*760c253cSXin Li # Build LLVM 306*760c253cSXin Li if not opts.skip_build: 307*760c253cSXin Li 308*760c253cSXin Li # Get llvm USE flags. 309*760c253cSXin Li use_flags = get_use_flags( 310*760c253cSXin Li opts.use_debug, opts.use_lto, opts.error_on_patch_failure 311*760c253cSXin Li ) 312*760c253cSXin Li 313*760c253cSXin Li # Reset LLVM if needed. 314*760c253cSXin Li if opts.reset_llvm: 315*760c253cSXin Li llvm_repo.reset() 316*760c253cSXin Li 317*760c253cSXin Li # Build new LLVM-9999. 318*760c253cSXin Li build_result = llvm_repo.build(use_flags) 319*760c253cSXin Li 320*760c253cSXin Li # Check LLVM-9999 build. 321*760c253cSXin Li if opts.test_llvm_build: 322*760c253cSXin Li logging.info("Checking result of build....") 323*760c253cSXin Li build_result.exit_assert(opts.search_error, llvm_hash, opts.log_dir) 324*760c253cSXin Li elif build_result.success(): 325*760c253cSXin Li logging.info("LLVM candidate built successfully.") 326*760c253cSXin Li else: 327*760c253cSXin Li logging.error("LLVM could not be built.") 328*760c253cSXin Li logging.info("Completed bisection stage with: SKIP.") 329*760c253cSXin Li sys.exit(EXIT_SKIP) 330*760c253cSXin Li 331*760c253cSXin Li # Run Test Command. 332*760c253cSXin Li test_result = run_test(opts.test) 333*760c253cSXin Li logging.info("Checking result of test command....") 334*760c253cSXin Li test_result.exit_assert(opts.search_error, llvm_hash, log_dir) 335*760c253cSXin Li 336*760c253cSXin Li 337*760c253cSXin Lidef main(): 338*760c253cSXin Li logging.basicConfig(level=logging.INFO) 339*760c253cSXin Li chroot.VerifyInsideChroot() 340*760c253cSXin Li opts = get_args() 341*760c253cSXin Li try: 342*760c253cSXin Li run(opts) 343*760c253cSXin Li except AbortingException: 344*760c253cSXin Li abort(opts.never_abort) 345*760c253cSXin Li except Exception: 346*760c253cSXin Li logging.exception("Uncaught Exception in main") 347*760c253cSXin Li abort(opts.never_abort) 348*760c253cSXin Li 349*760c253cSXin Li 350*760c253cSXin Liif __name__ == "__main__": 351*760c253cSXin Li main() 352