xref: /aosp_15_r20/external/toolchain-utils/llvm_tools/llvm_simple_bisect.py (revision 760c253c1ed00ce9abd48f8546f08516e57485fe)
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