1*760c253cSXin Li#!/usr/bin/env python3 2*760c253cSXin Li# Copyright 2022 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"""check_clang_diags monitors for new diagnostics in LLVM 7*760c253cSXin Li 8*760c253cSXin LiThis looks at projects we care about (currently only clang-tidy, though 9*760c253cSXin Lihopefully clang in the future, too?) and files bugs whenever a new check or 10*760c253cSXin Liwarning appears. These bugs are intended to keep us up-to-date with new 11*760c253cSXin Lidiagnostics, so we can enable them as they land. 12*760c253cSXin Li""" 13*760c253cSXin Li 14*760c253cSXin Liimport argparse 15*760c253cSXin Liimport json 16*760c253cSXin Liimport logging 17*760c253cSXin Liimport os 18*760c253cSXin Liimport shutil 19*760c253cSXin Liimport subprocess 20*760c253cSXin Liimport sys 21*760c253cSXin Liimport textwrap 22*760c253cSXin Lifrom typing import Dict, List, Tuple 23*760c253cSXin Li 24*760c253cSXin Lifrom cros_utils import bugs 25*760c253cSXin Li 26*760c253cSXin Li 27*760c253cSXin Li_DEFAULT_ASSIGNEE = "mage" 28*760c253cSXin Li_DEFAULT_CCS = ["[email protected]"] 29*760c253cSXin Li 30*760c253cSXin Li 31*760c253cSXin Li# FIXME: clang would be cool to check, too? Doesn't seem to have a super stable 32*760c253cSXin Li# way of listing all warnings, unfortunately. 33*760c253cSXin Lidef _build_llvm(llvm_dir: str, build_dir: str) -> None: 34*760c253cSXin Li """Builds everything that _collect_available_diagnostics depends on.""" 35*760c253cSXin Li targets = ["clang-tidy"] 36*760c253cSXin Li # use `-C $llvm_dir` so the failure is easier to handle if llvm_dir DNE. 37*760c253cSXin Li ninja_result = subprocess.run( 38*760c253cSXin Li ["ninja", "-C", build_dir] + targets, 39*760c253cSXin Li check=False, 40*760c253cSXin Li ) 41*760c253cSXin Li if not ninja_result.returncode: 42*760c253cSXin Li return 43*760c253cSXin Li 44*760c253cSXin Li # Sometimes the directory doesn't exist, sometimes incremental cmake 45*760c253cSXin Li # breaks, sometimes something random happens. Start fresh since that fixes 46*760c253cSXin Li # the issue most of the time. 47*760c253cSXin Li logging.warning("Initial build failed; trying to build from scratch.") 48*760c253cSXin Li shutil.rmtree(build_dir, ignore_errors=True) 49*760c253cSXin Li os.makedirs(build_dir) 50*760c253cSXin Li subprocess.run( 51*760c253cSXin Li [ 52*760c253cSXin Li "cmake", 53*760c253cSXin Li "-G", 54*760c253cSXin Li "Ninja", 55*760c253cSXin Li "-DCMAKE_BUILD_TYPE=MinSizeRel", 56*760c253cSXin Li "-DLLVM_USE_LINKER=lld", 57*760c253cSXin Li "-DLLVM_ENABLE_PROJECTS=clang;clang-tools-extra", 58*760c253cSXin Li "-DLLVM_TARGETS_TO_BUILD=X86", 59*760c253cSXin Li f"{os.path.abspath(llvm_dir)}/llvm", 60*760c253cSXin Li ], 61*760c253cSXin Li cwd=build_dir, 62*760c253cSXin Li check=True, 63*760c253cSXin Li ) 64*760c253cSXin Li subprocess.run(["ninja"] + targets, check=True, cwd=build_dir) 65*760c253cSXin Li 66*760c253cSXin Li 67*760c253cSXin Lidef _collect_available_diagnostics( 68*760c253cSXin Li llvm_dir: str, build_dir: str 69*760c253cSXin Li) -> Dict[str, List[str]]: 70*760c253cSXin Li _build_llvm(llvm_dir, build_dir) 71*760c253cSXin Li 72*760c253cSXin Li clang_tidy = os.path.join(os.path.abspath(build_dir), "bin", "clang-tidy") 73*760c253cSXin Li clang_tidy_checks = subprocess.run( 74*760c253cSXin Li [clang_tidy, "-checks=*", "-list-checks"], 75*760c253cSXin Li # Use cwd='/' to ensure no .clang-tidy files are picked up. It 76*760c253cSXin Li # _shouldn't_ matter, but it's also ~free, so... 77*760c253cSXin Li check=True, 78*760c253cSXin Li cwd="/", 79*760c253cSXin Li stdout=subprocess.PIPE, 80*760c253cSXin Li encoding="utf-8", 81*760c253cSXin Li ) 82*760c253cSXin Li clang_tidy_checks_stdout = [ 83*760c253cSXin Li x.strip() for x in clang_tidy_checks.stdout.strip().splitlines() 84*760c253cSXin Li ] 85*760c253cSXin Li 86*760c253cSXin Li # The first line should always be this, then each line thereafter is a check 87*760c253cSXin Li # name. 88*760c253cSXin Li assert ( 89*760c253cSXin Li clang_tidy_checks_stdout[0] == "Enabled checks:" 90*760c253cSXin Li ), clang_tidy_checks_stdout 91*760c253cSXin Li available_checks = clang_tidy_checks_stdout[1:] 92*760c253cSXin Li assert not any( 93*760c253cSXin Li check.isspace() for check in available_checks 94*760c253cSXin Li ), clang_tidy_checks 95*760c253cSXin Li return {"clang-tidy": available_checks} 96*760c253cSXin Li 97*760c253cSXin Li 98*760c253cSXin Lidef _process_new_diagnostics( 99*760c253cSXin Li old: Dict[str, List[str]], new: Dict[str, List[str]] 100*760c253cSXin Li) -> Tuple[Dict[str, List[str]], Dict[str, List[str]]]: 101*760c253cSXin Li """Determines the set of new diagnostics that we should file bugs for. 102*760c253cSXin Li 103*760c253cSXin Li old: The previous state that this function returned as `new_state_file`, or 104*760c253cSXin Li `{}` 105*760c253cSXin Li new: The diagnostics that we've most recently found. This is a dict in the 106*760c253cSXin Li form {tool: [diag]} 107*760c253cSXin Li 108*760c253cSXin Li Returns a `new_state_file` to pass into this function as `old` in the 109*760c253cSXin Li future, and a dict of diags to file bugs about. 110*760c253cSXin Li """ 111*760c253cSXin Li new_diagnostics = {} 112*760c253cSXin Li new_state_file = {} 113*760c253cSXin Li for tool, diags in new.items(): 114*760c253cSXin Li if tool not in old: 115*760c253cSXin Li logging.info( 116*760c253cSXin Li "New tool with diagnostics: %s; pretending none are new", tool 117*760c253cSXin Li ) 118*760c253cSXin Li new_state_file[tool] = diags 119*760c253cSXin Li else: 120*760c253cSXin Li old_diags = set(old[tool]) 121*760c253cSXin Li newly_added_diags = [x for x in diags if x not in old_diags] 122*760c253cSXin Li if newly_added_diags: 123*760c253cSXin Li new_diagnostics[tool] = newly_added_diags 124*760c253cSXin Li # This specifically tries to make diags sticky: if one is landed, 125*760c253cSXin Li # then reverted, then relanded, we ignore the reland. This might 126*760c253cSXin Li # not be desirable? I don't know. 127*760c253cSXin Li new_state_file[tool] = old[tool] + newly_added_diags 128*760c253cSXin Li 129*760c253cSXin Li # Sort things so we have more predictable output. 130*760c253cSXin Li for v in new_diagnostics.values(): 131*760c253cSXin Li v.sort() 132*760c253cSXin Li 133*760c253cSXin Li return new_state_file, new_diagnostics 134*760c253cSXin Li 135*760c253cSXin Li 136*760c253cSXin Lidef _file_bugs_for_new_diags(new_diags: Dict[str, List[str]]): 137*760c253cSXin Li for tool, diags in sorted(new_diags.items()): 138*760c253cSXin Li for diag in diags: 139*760c253cSXin Li bugs.CreateNewBug( 140*760c253cSXin Li component_id=bugs.WellKnownComponents.CrOSToolchainPublic, 141*760c253cSXin Li title=f"Investigate {tool} check `{diag}`", 142*760c253cSXin Li body=textwrap.dedent( 143*760c253cSXin Li f"""\ 144*760c253cSXin Li It seems that the `{diag}` check was recently added 145*760c253cSXin Li to {tool}. It's probably good to TAL at whether this 146*760c253cSXin Li check would be good for us to enable in e.g., platform2, or 147*760c253cSXin Li across ChromeOS. 148*760c253cSXin Li """ 149*760c253cSXin Li ), 150*760c253cSXin Li assignee=_DEFAULT_ASSIGNEE, 151*760c253cSXin Li cc=_DEFAULT_CCS, 152*760c253cSXin Li ) 153*760c253cSXin Li 154*760c253cSXin Li 155*760c253cSXin Lidef main(argv: List[str]) -> None: 156*760c253cSXin Li logging.basicConfig( 157*760c253cSXin Li format=">> %(asctime)s: %(levelname)s: %(filename)s:%(lineno)d: " 158*760c253cSXin Li "%(message)s", 159*760c253cSXin Li level=logging.INFO, 160*760c253cSXin Li ) 161*760c253cSXin Li 162*760c253cSXin Li parser = argparse.ArgumentParser( 163*760c253cSXin Li description=__doc__, 164*760c253cSXin Li formatter_class=argparse.RawDescriptionHelpFormatter, 165*760c253cSXin Li ) 166*760c253cSXin Li parser.add_argument( 167*760c253cSXin Li "--llvm_dir", required=True, help="LLVM directory to check. Required." 168*760c253cSXin Li ) 169*760c253cSXin Li parser.add_argument( 170*760c253cSXin Li "--llvm_build_dir", 171*760c253cSXin Li required=True, 172*760c253cSXin Li help="Build directory for LLVM. Required & autocreated.", 173*760c253cSXin Li ) 174*760c253cSXin Li parser.add_argument( 175*760c253cSXin Li "--state_file", 176*760c253cSXin Li required=True, 177*760c253cSXin Li help="State file to use to suppress duplicate complaints. Required.", 178*760c253cSXin Li ) 179*760c253cSXin Li parser.add_argument( 180*760c253cSXin Li "--dry_run", 181*760c253cSXin Li action="store_true", 182*760c253cSXin Li help="Skip filing bugs & writing to the state file; just log " 183*760c253cSXin Li "differences.", 184*760c253cSXin Li ) 185*760c253cSXin Li opts = parser.parse_args(argv) 186*760c253cSXin Li 187*760c253cSXin Li build_dir = opts.llvm_build_dir 188*760c253cSXin Li dry_run = opts.dry_run 189*760c253cSXin Li llvm_dir = opts.llvm_dir 190*760c253cSXin Li state_file = opts.state_file 191*760c253cSXin Li 192*760c253cSXin Li try: 193*760c253cSXin Li with open(state_file, encoding="utf-8") as f: 194*760c253cSXin Li prior_diagnostics = json.load(f) 195*760c253cSXin Li except FileNotFoundError: 196*760c253cSXin Li # If the state file didn't exist, just create it without complaining 197*760c253cSXin Li # this time. 198*760c253cSXin Li prior_diagnostics = {} 199*760c253cSXin Li 200*760c253cSXin Li available_diagnostics = _collect_available_diagnostics(llvm_dir, build_dir) 201*760c253cSXin Li logging.info("Available diagnostics are %s", available_diagnostics) 202*760c253cSXin Li if available_diagnostics == prior_diagnostics: 203*760c253cSXin Li logging.info("Current diagnostics are identical to previous ones; quit") 204*760c253cSXin Li return 205*760c253cSXin Li 206*760c253cSXin Li new_state_file, new_diagnostics = _process_new_diagnostics( 207*760c253cSXin Li prior_diagnostics, available_diagnostics 208*760c253cSXin Li ) 209*760c253cSXin Li logging.info("New diagnostics in existing tool(s): %s", new_diagnostics) 210*760c253cSXin Li 211*760c253cSXin Li if dry_run: 212*760c253cSXin Li logging.info( 213*760c253cSXin Li "Skipping new state file writing and bug filing; dry-run " 214*760c253cSXin Li "mode wins" 215*760c253cSXin Li ) 216*760c253cSXin Li else: 217*760c253cSXin Li _file_bugs_for_new_diags(new_diagnostics) 218*760c253cSXin Li new_state_file_path = state_file + ".new" 219*760c253cSXin Li with open(new_state_file_path, "w", encoding="utf-8") as f: 220*760c253cSXin Li json.dump(new_state_file, f) 221*760c253cSXin Li os.rename(new_state_file_path, state_file) 222*760c253cSXin Li 223*760c253cSXin Li 224*760c253cSXin Liif __name__ == "__main__": 225*760c253cSXin Li main(sys.argv[1:]) 226