1#!/usr/bin/env python3 2 3# Copyright (c) 2018,2020 Intel Corporation 4# Copyright (c) 2022 Nordic Semiconductor ASA 5# SPDX-License-Identifier: Apache-2.0 6 7import argparse 8import collections 9from itertools import takewhile 10import json 11import logging 12import os 13from pathlib import Path 14import platform 15import re 16import subprocess 17import sys 18import tempfile 19import traceback 20import shlex 21import shutil 22import textwrap 23import unidiff 24 25from yamllint import config, linter 26 27from junitparser import TestCase, TestSuite, JUnitXml, Skipped, Error, Failure 28import magic 29 30from west.manifest import Manifest 31from west.manifest import ManifestProject 32 33sys.path.insert(0, str(Path(__file__).resolve().parents[1])) 34from get_maintainer import Maintainers, MaintainersError 35import list_boards 36import list_hardware 37 38logger = None 39 40def git(*args, cwd=None, ignore_non_zero=False): 41 # Helper for running a Git command. Returns the rstrip()ed stdout output. 42 # Called like git("diff"). Exits with SystemError (raised by sys.exit()) on 43 # errors if 'ignore_non_zero' is set to False (default: False). 'cwd' is the 44 # working directory to use (default: current directory). 45 46 git_cmd = ("git",) + args 47 try: 48 cp = subprocess.run(git_cmd, capture_output=True, cwd=cwd) 49 except OSError as e: 50 err(f"failed to run '{cmd2str(git_cmd)}': {e}") 51 52 if not ignore_non_zero and (cp.returncode or cp.stderr): 53 err(f"'{cmd2str(git_cmd)}' exited with status {cp.returncode} and/or " 54 f"wrote to stderr.\n" 55 f"==stdout==\n" 56 f"{cp.stdout.decode('utf-8')}\n" 57 f"==stderr==\n" 58 f"{cp.stderr.decode('utf-8')}\n") 59 60 return cp.stdout.decode("utf-8").rstrip() 61 62def get_shas(refspec): 63 """ 64 Returns the list of Git SHAs for 'refspec'. 65 66 :param refspec: 67 :return: 68 """ 69 return git('rev-list', 70 f'--max-count={-1 if "." in refspec else 1}', refspec).split() 71 72def get_files(filter=None, paths=None): 73 filter_arg = (f'--diff-filter={filter}',) if filter else () 74 paths_arg = ('--', *paths) if paths else () 75 out = git('diff', '--name-only', *filter_arg, COMMIT_RANGE, *paths_arg) 76 files = out.splitlines() 77 for file in list(files): 78 if not os.path.isfile(os.path.join(GIT_TOP, file)): 79 # Drop submodule directories from the list. 80 files.remove(file) 81 return files 82 83class FmtdFailure(Failure): 84 def __init__( 85 self, severity, title, file, line=None, col=None, desc="", end_line=None, end_col=None 86 ): 87 self.severity = severity 88 self.title = title 89 self.file = file 90 self.line = line 91 self.col = col 92 self.end_line = end_line 93 self.end_col = end_col 94 self.desc = desc 95 description = f':{desc}' if desc else '' 96 msg_body = desc or title 97 98 txt = f'\n{title}{description}\nFile:{file}' + \ 99 (f'\nLine:{line}' if line else '') + \ 100 (f'\nColumn:{col}' if col else '') + \ 101 (f'\nEndLine:{end_line}' if end_line else '') + \ 102 (f'\nEndColumn:{end_col}' if end_col else '') 103 msg = f'{file}' + (f':{line}' if line else '') + f' {msg_body}' 104 typ = severity.lower() 105 106 super().__init__(msg, typ) 107 108 self.text = txt 109 110 111class ComplianceTest: 112 """ 113 Base class for tests. Inheriting classes should have a run() method and set 114 these class variables: 115 116 name: 117 Test name 118 119 doc: 120 Link to documentation related to what's being tested 121 122 path_hint: 123 The path the test runs itself in. This is just informative and used in 124 the message that gets printed when running the test. 125 126 There are two magic strings that can be used instead of a path: 127 - The magic string "<zephyr-base>" can be used to refer to the 128 environment variable ZEPHYR_BASE or, when missing, the calculated base of 129 the zephyr tree 130 - The magic string "<git-top>" refers to the top-level repository 131 directory. This avoids running 'git' to find the top-level directory 132 before main() runs (class variable assignments run when the 'class ...' 133 statement runs). That avoids swallowing errors, because main() reports 134 them to GitHub 135 """ 136 def __init__(self): 137 self.case = TestCase(type(self).name, "Guidelines") 138 # This is necessary because Failure can be subclassed, but since it is 139 # always restored form the element tree, the subclass is lost upon 140 # restoring 141 self.fmtd_failures = [] 142 143 def _result(self, res, text): 144 res.text = text.rstrip() 145 self.case.result += [res] 146 147 def error(self, text, msg=None, type_="error"): 148 """ 149 Signals a problem with running the test, with message 'msg'. 150 151 Raises an exception internally, so you do not need to put a 'return' 152 after error(). 153 """ 154 err = Error(msg or f'{type(self).name} error', type_) 155 self._result(err, text) 156 157 raise EndTest 158 159 def skip(self, text, msg=None, type_="skip"): 160 """ 161 Signals that the test should be skipped, with message 'msg'. 162 163 Raises an exception internally, so you do not need to put a 'return' 164 after skip(). 165 """ 166 skpd = Skipped(msg or f'{type(self).name} skipped', type_) 167 self._result(skpd, text) 168 169 raise EndTest 170 171 def failure(self, text, msg=None, type_="failure"): 172 """ 173 Signals that the test failed, with message 'msg'. Can be called many 174 times within the same test to report multiple failures. 175 """ 176 fail = Failure(msg or f'{type(self).name} issues', type_) 177 self._result(fail, text) 178 179 def fmtd_failure( 180 self, severity, title, file, line=None, col=None, desc="", end_line=None, end_col=None 181 ): 182 """ 183 Signals that the test failed, and store the information in a formatted 184 standardized manner. Can be called many times within the same test to 185 report multiple failures. 186 """ 187 fail = FmtdFailure(severity, title, file, line, col, desc, end_line, end_col) 188 self._result(fail, fail.text) 189 self.fmtd_failures.append(fail) 190 191class EndTest(Exception): 192 """ 193 Raised by ComplianceTest.error()/skip() to end the test. 194 195 Tests can raise EndTest themselves to immediately end the test, e.g. from 196 within a nested function call. 197 """ 198 199 200class CheckPatch(ComplianceTest): 201 """ 202 Runs checkpatch and reports found issues 203 204 """ 205 name = "Checkpatch" 206 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#coding-style for more details." 207 path_hint = "<git-top>" 208 209 def run(self): 210 checkpatch = os.path.join(ZEPHYR_BASE, 'scripts', 'checkpatch.pl') 211 if not os.path.exists(checkpatch): 212 self.skip(f'{checkpatch} not found') 213 214 # check for Perl installation on Windows 215 if os.name == 'nt': 216 if not shutil.which('perl'): 217 self.failure("Perl not installed - required for checkpatch.pl. Please install Perl or add to PATH.") 218 return 219 else: 220 cmd = ['perl', checkpatch] 221 222 # Linux and MacOS 223 else: 224 cmd = [checkpatch] 225 226 cmd.extend(['--mailback', '--no-tree', '-']) 227 diff = subprocess.Popen(('git', 'diff', '--no-ext-diff', COMMIT_RANGE), 228 stdout=subprocess.PIPE, 229 cwd=GIT_TOP) 230 try: 231 subprocess.run(cmd, 232 check=True, 233 stdin=diff.stdout, 234 stdout=subprocess.PIPE, 235 stderr=subprocess.STDOUT, 236 shell=False, cwd=GIT_TOP) 237 238 except subprocess.CalledProcessError as ex: 239 output = ex.output.decode("utf-8") 240 regex = r'^\s*\S+:(\d+):\s*(ERROR|WARNING):(.+?):(.+)(?:\n|\r\n?)+' \ 241 r'^\s*#(\d+):\s*FILE:\s*(.+):(\d+):' 242 243 matches = re.findall(regex, output, re.MULTILINE) 244 245 # add a guard here for excessive number of errors, do not try and 246 # process each one of them and instead push this as one failure. 247 if len(matches) > 500: 248 self.failure(output) 249 return 250 251 for m in matches: 252 self.fmtd_failure(m[1].lower(), m[2], m[5], m[6], col=None, 253 desc=m[3]) 254 255 # If the regex has not matched add the whole output as a failure 256 if len(matches) == 0: 257 self.failure(output) 258 259 260class BoardYmlCheck(ComplianceTest): 261 """ 262 Check the board.yml files 263 """ 264 name = "BoardYml" 265 doc = "Check the board.yml file format" 266 path_hint = "<zephyr-base>" 267 268 def check_board_file(self, file, vendor_prefixes): 269 """Validate a single board file.""" 270 with open(file) as fp: 271 for line_num, line in enumerate(fp.readlines(), start=1): 272 if "vendor:" in line: 273 _, vnd = line.strip().split(":", 2) 274 vnd = vnd.strip() 275 if vnd not in vendor_prefixes: 276 desc = f"invalid vendor: {vnd}" 277 self.fmtd_failure("error", "BoardYml", file, line_num, 278 desc=desc) 279 280 def run(self): 281 vendor_prefixes = ["others"] 282 with open(os.path.join(ZEPHYR_BASE, "dts", "bindings", "vendor-prefixes.txt")) as fp: 283 for line in fp.readlines(): 284 line = line.strip() 285 if not line or line.startswith("#"): 286 continue 287 try: 288 vendor, _ = line.split("\t", 2) 289 vendor_prefixes.append(vendor) 290 except ValueError: 291 self.error(f"Invalid line in vendor-prefixes.txt:\"{line}\".") 292 self.error("Did you forget the tab character?") 293 294 path = Path(ZEPHYR_BASE) 295 for file in path.glob("**/board.yml"): 296 self.check_board_file(file, vendor_prefixes) 297 298 299class ClangFormatCheck(ComplianceTest): 300 """ 301 Check if clang-format reports any issues 302 """ 303 name = "ClangFormat" 304 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#clang-format for more details." 305 path_hint = "<git-top>" 306 307 def run(self): 308 exe = f"clang-format-diff.{'exe' if platform.system() == 'Windows' else 'py'}" 309 310 for file in get_files(): 311 if Path(file).suffix not in ['.c', '.h']: 312 continue 313 314 diff = subprocess.Popen(('git', 'diff', '-U0', '--no-color', COMMIT_RANGE, '--', file), 315 stdout=subprocess.PIPE, 316 cwd=GIT_TOP) 317 try: 318 subprocess.run((exe, '-p1'), 319 check=True, 320 stdin=diff.stdout, 321 stdout=subprocess.PIPE, 322 stderr=subprocess.STDOUT, 323 cwd=GIT_TOP) 324 325 except subprocess.CalledProcessError as ex: 326 patchset = unidiff.PatchSet.from_string(ex.output, encoding="utf-8") 327 for patch in patchset: 328 for hunk in patch: 329 # Strip the before and after context 330 before = next(i for i,v in enumerate(hunk) if str(v).startswith(('-', '+'))) 331 after = next(i for i,v in enumerate(reversed(hunk)) if str(v).startswith(('-', '+'))) 332 msg = "".join([str(l) for l in hunk[before:-after or None]]) 333 334 # show the hunk at the last line 335 self.fmtd_failure("notice", 336 "You may want to run clang-format on this change", 337 file, line=hunk.source_start + hunk.source_length - after, 338 desc=f'\r\n{msg}') 339 340 341class DevicetreeBindingsCheck(ComplianceTest): 342 """ 343 Checks if we are introducing any unwanted properties in Devicetree Bindings. 344 """ 345 name = "DevicetreeBindings" 346 doc = "See https://docs.zephyrproject.org/latest/build/dts/bindings.html for more details." 347 path_hint = "<zephyr-base>" 348 349 def run(self, full=True): 350 dts_bindings = self.parse_dt_bindings() 351 352 for dts_binding in dts_bindings: 353 self.required_false_check(dts_binding) 354 355 def parse_dt_bindings(self): 356 """ 357 Returns a list of dts/bindings/**/*.yaml files 358 """ 359 360 dt_bindings = [] 361 for file_name in get_files(filter="d"): 362 if 'dts/bindings/' in file_name and file_name.endswith('.yaml'): 363 dt_bindings.append(file_name) 364 365 return dt_bindings 366 367 def required_false_check(self, dts_binding): 368 with open(dts_binding) as file: 369 for line_number, line in enumerate(file, 1): 370 if 'required: false' in line: 371 self.fmtd_failure( 372 'warning', 'Devicetree Bindings', dts_binding, 373 line_number, col=None, 374 desc="'required: false' is redundant, please remove") 375 376 377class KconfigCheck(ComplianceTest): 378 """ 379 Checks is we are introducing any new warnings/errors with Kconfig, 380 for example using undefined Kconfig variables. 381 """ 382 name = "Kconfig" 383 doc = "See https://docs.zephyrproject.org/latest/build/kconfig/tips.html for more details." 384 path_hint = "<zephyr-base>" 385 386 # Top-level Kconfig file. The path can be relative to srctree (ZEPHYR_BASE). 387 FILENAME = "Kconfig" 388 389 # Kconfig symbol prefix/namespace. 390 CONFIG_ = "CONFIG_" 391 392 def run(self): 393 kconf = self.parse_kconfig() 394 395 self.check_top_menu_not_too_long(kconf) 396 self.check_no_pointless_menuconfigs(kconf) 397 self.check_no_undef_within_kconfig(kconf) 398 self.check_no_redefined_in_defconfig(kconf) 399 self.check_no_enable_in_boolean_prompt(kconf) 400 self.check_soc_name_sync(kconf) 401 self.check_no_undef_outside_kconfig(kconf) 402 self.check_disallowed_defconfigs(kconf) 403 404 def get_modules(self, modules_file, sysbuild_modules_file, settings_file): 405 """ 406 Get a list of modules and put them in a file that is parsed by 407 Kconfig 408 409 This is needed to complete Kconfig sanity tests. 410 411 """ 412 # Invoke the script directly using the Python executable since this is 413 # not a module nor a pip-installed Python utility 414 zephyr_module_path = os.path.join(ZEPHYR_BASE, "scripts", 415 "zephyr_module.py") 416 cmd = [sys.executable, zephyr_module_path, 417 '--kconfig-out', modules_file, 418 '--sysbuild-kconfig-out', sysbuild_modules_file, 419 '--settings-out', settings_file] 420 try: 421 subprocess.run(cmd, check=True, stdout=subprocess.PIPE, 422 stderr=subprocess.STDOUT) 423 except subprocess.CalledProcessError as ex: 424 self.error(ex.output.decode("utf-8")) 425 426 modules_dir = ZEPHYR_BASE + '/modules' 427 modules = [name for name in os.listdir(modules_dir) if 428 os.path.exists(os.path.join(modules_dir, name, 'Kconfig'))] 429 430 with open(modules_file, 'r') as fp_module_file: 431 content = fp_module_file.read() 432 433 with open(modules_file, 'w') as fp_module_file: 434 for module in modules: 435 fp_module_file.write("ZEPHYR_{}_KCONFIG = {}\n".format( 436 re.sub('[^a-zA-Z0-9]', '_', module).upper(), 437 modules_dir + '/' + module + '/Kconfig' 438 )) 439 fp_module_file.write(content) 440 441 def get_module_setting_root(self, root, settings_file): 442 """ 443 Parse the Zephyr module generated settings file given by 'settings_file' 444 and return all root settings defined by 'root'. 445 """ 446 # Invoke the script directly using the Python executable since this is 447 # not a module nor a pip-installed Python utility 448 root_paths = [] 449 450 if os.path.exists(settings_file): 451 with open(settings_file, 'r') as fp_setting_file: 452 content = fp_setting_file.read() 453 454 lines = content.strip().split('\n') 455 for line in lines: 456 root = root.upper() 457 if line.startswith(f'"{root}_ROOT":'): 458 _, root_path = line.split(":", 1) 459 root_paths.append(Path(root_path.strip('"'))) 460 return root_paths 461 462 def get_kconfig_dts(self, kconfig_dts_file, settings_file): 463 """ 464 Generate the Kconfig.dts using dts/bindings as the source. 465 466 This is needed to complete Kconfig compliance tests. 467 468 """ 469 # Invoke the script directly using the Python executable since this is 470 # not a module nor a pip-installed Python utility 471 zephyr_drv_kconfig_path = os.path.join(ZEPHYR_BASE, "scripts", "dts", 472 "gen_driver_kconfig_dts.py") 473 binding_paths = [] 474 binding_paths.append(os.path.join(ZEPHYR_BASE, "dts", "bindings")) 475 476 dts_root_paths = self.get_module_setting_root('dts', settings_file) 477 for p in dts_root_paths: 478 binding_paths.append(p / "dts" / "bindings") 479 480 cmd = [sys.executable, zephyr_drv_kconfig_path, 481 '--kconfig-out', kconfig_dts_file, '--bindings-dirs'] 482 for binding_path in binding_paths: 483 cmd.append(binding_path) 484 try: 485 subprocess.run(cmd, check=True, stdout=subprocess.PIPE, 486 stderr=subprocess.STDOUT) 487 except subprocess.CalledProcessError as ex: 488 self.error(ex.output.decode("utf-8")) 489 490 def get_v2_model(self, kconfig_dir, settings_file): 491 """ 492 Get lists of v2 boards and SoCs and put them in a file that is parsed by 493 Kconfig 494 495 This is needed to complete Kconfig sanity tests. 496 """ 497 os.environ['HWM_SCHEME'] = 'v2' 498 os.environ["KCONFIG_BOARD_DIR"] = os.path.join(kconfig_dir, 'boards') 499 500 os.makedirs(os.path.join(kconfig_dir, 'boards'), exist_ok=True) 501 os.makedirs(os.path.join(kconfig_dir, 'soc'), exist_ok=True) 502 os.makedirs(os.path.join(kconfig_dir, 'arch'), exist_ok=True) 503 504 kconfig_file = os.path.join(kconfig_dir, 'boards', 'Kconfig') 505 kconfig_boards_file = os.path.join(kconfig_dir, 'boards', 'Kconfig.boards') 506 kconfig_sysbuild_file = os.path.join(kconfig_dir, 'boards', 'Kconfig.sysbuild') 507 kconfig_defconfig_file = os.path.join(kconfig_dir, 'boards', 'Kconfig.defconfig') 508 509 board_roots = self.get_module_setting_root('board', settings_file) 510 board_roots.insert(0, Path(ZEPHYR_BASE)) 511 soc_roots = self.get_module_setting_root('soc', settings_file) 512 soc_roots.insert(0, Path(ZEPHYR_BASE)) 513 root_args = argparse.Namespace(**{'board_roots': board_roots, 514 'soc_roots': soc_roots, 'board': None, 515 'board_dir': []}) 516 v2_boards = list_boards.find_v2_boards(root_args).values() 517 518 with open(kconfig_defconfig_file, 'w') as fp: 519 for board in v2_boards: 520 for board_dir in board.directories: 521 fp.write('osource "' + (board_dir / 'Kconfig.defconfig').as_posix() + '"\n') 522 523 with open(kconfig_sysbuild_file, 'w') as fp: 524 for board in v2_boards: 525 for board_dir in board.directories: 526 fp.write('osource "' + (board_dir / 'Kconfig.sysbuild').as_posix() + '"\n') 527 528 with open(kconfig_boards_file, 'w') as fp: 529 for board in v2_boards: 530 board_str = 'BOARD_' + re.sub(r"[^a-zA-Z0-9_]", "_", board.name).upper() 531 fp.write('config ' + board_str + '\n') 532 fp.write('\t bool\n') 533 for qualifier in list_boards.board_v2_qualifiers(board): 534 board_str = ('BOARD_' + board.name + '_' + 535 re.sub(r"[^a-zA-Z0-9_]", "_", qualifier)).upper() 536 fp.write('config ' + board_str + '\n') 537 fp.write('\t bool\n') 538 for board_dir in board.directories: 539 fp.write( 540 'source "' + (board_dir / ('Kconfig.' + board.name)).as_posix() + '"\n' 541 ) 542 543 with open(kconfig_file, 'w') as fp: 544 for board in v2_boards: 545 for board_dir in board.directories: 546 fp.write('osource "' + (board_dir / 'Kconfig').as_posix() + '"\n') 547 548 kconfig_defconfig_file = os.path.join(kconfig_dir, 'soc', 'Kconfig.defconfig') 549 kconfig_sysbuild_file = os.path.join(kconfig_dir, 'soc', 'Kconfig.sysbuild') 550 kconfig_soc_file = os.path.join(kconfig_dir, 'soc', 'Kconfig.soc') 551 kconfig_file = os.path.join(kconfig_dir, 'soc', 'Kconfig') 552 553 root_args = argparse.Namespace(**{'soc_roots': soc_roots}) 554 v2_systems = list_hardware.find_v2_systems(root_args) 555 556 soc_folders = {folder for soc in v2_systems.get_socs() for folder in soc.folder} 557 with open(kconfig_defconfig_file, 'w') as fp: 558 for folder in soc_folders: 559 fp.write('osource "' + (Path(folder) / 'Kconfig.defconfig').as_posix() + '"\n') 560 561 with open(kconfig_sysbuild_file, 'w') as fp: 562 for folder in soc_folders: 563 fp.write('osource "' + (Path(folder) / 'Kconfig.sysbuild').as_posix() + '"\n') 564 565 with open(kconfig_soc_file, 'w') as fp: 566 for folder in soc_folders: 567 fp.write('source "' + (Path(folder) / 'Kconfig.soc').as_posix() + '"\n') 568 569 with open(kconfig_file, 'w') as fp: 570 for folder in soc_folders: 571 fp.write('source "' + (Path(folder) / 'Kconfig').as_posix() + '"\n') 572 573 kconfig_file = os.path.join(kconfig_dir, 'arch', 'Kconfig') 574 575 root_args = argparse.Namespace(**{'arch_roots': [Path(ZEPHYR_BASE)], 'arch': None}) 576 v2_archs = list_hardware.find_v2_archs(root_args) 577 578 with open(kconfig_file, 'w') as fp: 579 for arch in v2_archs['archs']: 580 fp.write('source "' + (Path(arch['path']) / 'Kconfig').as_posix() + '"\n') 581 582 def parse_kconfig(self): 583 """ 584 Returns a kconfiglib.Kconfig object for the Kconfig files. We reuse 585 this object for all tests to avoid having to reparse for each test. 586 """ 587 # Put the Kconfiglib path first to make sure no local Kconfiglib version is 588 # used 589 kconfig_path = os.path.join(ZEPHYR_BASE, "scripts", "kconfig") 590 if not os.path.exists(kconfig_path): 591 self.error(kconfig_path + " not found") 592 593 kconfiglib_dir = tempfile.mkdtemp(prefix="kconfiglib_") 594 595 sys.path.insert(0, kconfig_path) 596 # Import globally so that e.g. kconfiglib.Symbol can be referenced in 597 # tests 598 global kconfiglib 599 import kconfiglib 600 601 # Look up Kconfig files relative to ZEPHYR_BASE 602 os.environ["srctree"] = ZEPHYR_BASE 603 604 # Parse the entire Kconfig tree, to make sure we see all symbols 605 os.environ["SOC_DIR"] = "soc/" 606 os.environ["ARCH_DIR"] = "arch/" 607 os.environ["BOARD"] = "boards" 608 os.environ["ARCH"] = "*" 609 os.environ["KCONFIG_BINARY_DIR"] = kconfiglib_dir 610 os.environ['DEVICETREE_CONF'] = "dummy" 611 os.environ['TOOLCHAIN_HAS_NEWLIB'] = "y" 612 613 # Older name for DEVICETREE_CONF, for compatibility with older Zephyr 614 # versions that don't have the renaming 615 os.environ["GENERATED_DTS_BOARD_CONF"] = "dummy" 616 617 # For multi repo support 618 self.get_modules(os.path.join(kconfiglib_dir, "Kconfig.modules"), 619 os.path.join(kconfiglib_dir, "Kconfig.sysbuild.modules"), 620 os.path.join(kconfiglib_dir, "settings_file.txt")) 621 # For Kconfig.dts support 622 self.get_kconfig_dts(os.path.join(kconfiglib_dir, "Kconfig.dts"), 623 os.path.join(kconfiglib_dir, "settings_file.txt")) 624 # For hardware model support (board, soc, arch) 625 self.get_v2_model(kconfiglib_dir, os.path.join(kconfiglib_dir, "settings_file.txt")) 626 627 # Tells Kconfiglib to generate warnings for all references to undefined 628 # symbols within Kconfig files 629 os.environ["KCONFIG_WARN_UNDEF"] = "y" 630 631 try: 632 # Note this will both print warnings to stderr _and_ return 633 # them: so some warnings might get printed 634 # twice. "warn_to_stderr=False" could unfortunately cause 635 # some (other) warnings to never be printed. 636 return kconfiglib.Kconfig(filename=self.FILENAME) 637 except kconfiglib.KconfigError as e: 638 self.failure(str(e)) 639 raise EndTest 640 finally: 641 # Clean up the temporary directory 642 shutil.rmtree(kconfiglib_dir) 643 644 def get_logging_syms(self, kconf): 645 # Returns a set() with the names of the Kconfig symbols generated with 646 # logging template in samples/tests folders. The Kconfig symbols doesn't 647 # include `CONFIG_` and for each module declared there is one symbol 648 # per suffix created. 649 650 suffixes = [ 651 "_LOG_LEVEL", 652 "_LOG_LEVEL_DBG", 653 "_LOG_LEVEL_ERR", 654 "_LOG_LEVEL_INF", 655 "_LOG_LEVEL_WRN", 656 "_LOG_LEVEL_OFF", 657 "_LOG_LEVEL_INHERIT", 658 "_LOG_LEVEL_DEFAULT", 659 ] 660 661 # Warning: Needs to work with both --perl-regexp and the 're' module. 662 regex = r"^\s*(?:module\s*=\s*)([A-Z0-9_]+)\s*(?:#|$)" 663 664 # Grep samples/ and tests/ for symbol definitions 665 grep_stdout = git("grep", "-I", "-h", "--perl-regexp", regex, "--", 666 ":samples", ":tests", cwd=ZEPHYR_BASE) 667 668 names = re.findall(regex, grep_stdout, re.MULTILINE) 669 670 kconf_syms = [] 671 for name in names: 672 for suffix in suffixes: 673 kconf_syms.append(f"{name}{suffix}") 674 675 return set(kconf_syms) 676 677 def check_disallowed_defconfigs(self, kconf): 678 """ 679 Checks that there are no disallowed Kconfigs used in board/SoC defconfig files 680 """ 681 # Grep for symbol references. 682 # 683 # Example output line for a reference to CONFIG_FOO at line 17 of 684 # foo/bar.c: 685 # 686 # foo/bar.c<null>17<null>#ifdef CONFIG_FOO 687 # 688 # 'git grep --only-matching' would get rid of the surrounding context 689 # ('#ifdef '), but it was added fairly recently (second half of 2018), 690 # so we extract the references from each line ourselves instead. 691 # 692 # The regex uses word boundaries (\b) to isolate the reference, and 693 # negative lookahead to automatically allowlist the following: 694 # 695 # - ##, for token pasting (CONFIG_FOO_##X) 696 # 697 # - $, e.g. for CMake variable expansion (CONFIG_FOO_${VAR}) 698 # 699 # - @, e.g. for CMakes's configure_file() (CONFIG_FOO_@VAR@) 700 # 701 # - {, e.g. for Python scripts ("CONFIG_FOO_{}_BAR".format(...)") 702 # 703 # - *, meant for comments like '#endif /* CONFIG_FOO_* */ 704 705 disallowed_symbols = { 706 "PINCTRL": "Drivers requiring PINCTRL must SELECT it instead.", 707 } 708 709 disallowed_regex = "(" + "|".join(disallowed_symbols.keys()) + ")$" 710 711 # Warning: Needs to work with both --perl-regexp and the 're' module 712 regex_boards = r"\bCONFIG_[A-Z0-9_]+\b(?!\s*##|[$@{(.*])" 713 regex_socs = r"\bconfig\s+[A-Z0-9_]+$" 714 715 grep_stdout_boards = git("grep", "--line-number", "-I", "--null", 716 "--perl-regexp", regex_boards, "--", ":boards", 717 cwd=ZEPHYR_BASE) 718 grep_stdout_socs = git("grep", "--line-number", "-I", "--null", 719 "--perl-regexp", regex_socs, "--", ":soc", 720 cwd=ZEPHYR_BASE) 721 722 # Board processing 723 # splitlines() supports various line terminators 724 for grep_line in grep_stdout_boards.splitlines(): 725 path, lineno, line = grep_line.split("\0") 726 727 # Extract symbol references (might be more than one) within the line 728 for sym_name in re.findall(regex_boards, line): 729 sym_name = sym_name[len("CONFIG_"):] 730 # Only check in Kconfig fragment files, references might exist in documentation 731 if re.match(disallowed_regex, sym_name) and (path[-len("conf"):] == "conf" or 732 path[-len("defconfig"):] == "defconfig"): 733 reason = disallowed_symbols.get(sym_name) 734 self.fmtd_failure("error", "BoardDisallowedKconfigs", path, lineno, desc=f""" 735Found disallowed Kconfig symbol in board Kconfig files: CONFIG_{sym_name:35} 736{reason} 737""") 738 739 # SoCs processing 740 # splitlines() supports various line terminators 741 for grep_line in grep_stdout_socs.splitlines(): 742 path, lineno, line = grep_line.split("\0") 743 744 # Extract symbol references (might be more than one) within the line 745 for sym_name in re.findall(regex_socs, line): 746 sym_name = sym_name[len("config"):].strip() 747 # Only check in Kconfig defconfig files 748 if re.match(disallowed_regex, sym_name) and "defconfig" in path: 749 reason = disallowed_symbols.get(sym_name, "Unknown reason") 750 self.fmtd_failure("error", "SoCDisallowedKconfigs", path, lineno, desc=f""" 751Found disallowed Kconfig symbol in SoC Kconfig files: {sym_name:35} 752{reason} 753""") 754 755 def get_defined_syms(self, kconf): 756 # Returns a set() with the names of all defined Kconfig symbols (with no 757 # 'CONFIG_' prefix). This is complicated by samples and tests defining 758 # their own Kconfig trees. For those, just grep for 'config FOO' to find 759 # definitions. Doing it "properly" with Kconfiglib is still useful for 760 # the main tree, because some symbols are defined using preprocessor 761 # macros. 762 763 # Warning: Needs to work with both --perl-regexp and the 're' module. 764 # (?:...) is a non-capturing group. 765 regex = r"^\s*(?:menu)?config\s*([A-Z0-9_]+)\s*(?:#|$)" 766 767 # Grep samples/ and tests/ for symbol definitions 768 grep_stdout = git("grep", "-I", "-h", "--perl-regexp", regex, "--", 769 ":samples", ":tests", cwd=ZEPHYR_BASE) 770 771 # Generate combined list of configs and choices from the main Kconfig tree. 772 kconf_syms = kconf.unique_defined_syms + kconf.unique_choices 773 774 # Symbols from the main Kconfig tree + grepped definitions from samples 775 # and tests 776 return set( 777 [sym.name for sym in kconf_syms] 778 + re.findall(regex, grep_stdout, re.MULTILINE) 779 ).union(self.get_logging_syms(kconf)) 780 781 def check_top_menu_not_too_long(self, kconf): 782 """ 783 Checks that there aren't too many items in the top-level menu (which 784 might be a sign that stuff accidentally got added there) 785 """ 786 max_top_items = 50 787 788 n_top_items = 0 789 node = kconf.top_node.list 790 while node: 791 # Only count items with prompts. Other items will never be 792 # shown in the menuconfig (outside show-all mode). 793 if node.prompt: 794 n_top_items += 1 795 node = node.next 796 797 if n_top_items > max_top_items: 798 self.failure(f""" 799Expected no more than {max_top_items} potentially visible items (items with 800prompts) in the top-level Kconfig menu, found {n_top_items} items. If you're 801deliberately adding new entries, then bump the 'max_top_items' variable in 802{__file__}.""") 803 804 def check_no_redefined_in_defconfig(self, kconf): 805 # Checks that no symbols are (re)defined in defconfigs. 806 807 for node in kconf.node_iter(): 808 # 'kconfiglib' is global 809 # pylint: disable=undefined-variable 810 if "defconfig" in node.filename and (node.prompt or node.help): 811 name = (node.item.name if node.item not in 812 (kconfiglib.MENU, kconfiglib.COMMENT) else str(node)) 813 self.failure(f""" 814Kconfig node '{name}' found with prompt or help in {node.filename}. 815Options must not be defined in defconfig files. 816""") 817 continue 818 819 def check_no_enable_in_boolean_prompt(self, kconf): 820 # Checks that boolean's prompt does not start with "Enable...". 821 822 for node in kconf.node_iter(): 823 # skip Kconfig nodes not in-tree (will present an absolute path) 824 if os.path.isabs(node.filename): 825 continue 826 827 # 'kconfiglib' is global 828 # pylint: disable=undefined-variable 829 830 # only process boolean symbols with a prompt 831 if (not isinstance(node.item, kconfiglib.Symbol) or 832 node.item.type != kconfiglib.BOOL or 833 not node.prompt or 834 not node.prompt[0]): 835 continue 836 837 if re.match(r"^[Ee]nable.*", node.prompt[0]): 838 self.failure(f""" 839Boolean option '{node.item.name}' prompt must not start with 'Enable...'. Please 840check Kconfig guidelines. 841""") 842 continue 843 844 def check_no_pointless_menuconfigs(self, kconf): 845 # Checks that there are no pointless 'menuconfig' symbols without 846 # children in the Kconfig files 847 848 bad_mconfs = [] 849 for node in kconf.node_iter(): 850 # 'kconfiglib' is global 851 # pylint: disable=undefined-variable 852 853 # Avoid flagging empty regular menus and choices, in case people do 854 # something with 'osource' (could happen for 'menuconfig' symbols 855 # too, though it's less likely) 856 if node.is_menuconfig and not node.list and \ 857 isinstance(node.item, kconfiglib.Symbol): 858 859 bad_mconfs.append(node) 860 861 if bad_mconfs: 862 self.failure("""\ 863Found pointless 'menuconfig' symbols without children. Use regular 'config' 864symbols instead. See 865https://docs.zephyrproject.org/latest/build/kconfig/tips.html#menuconfig-symbols. 866 867""" + "\n".join(f"{node.item.name:35} {node.filename}:{node.linenr}" 868 for node in bad_mconfs)) 869 870 def check_no_undef_within_kconfig(self, kconf): 871 """ 872 Checks that there are no references to undefined Kconfig symbols within 873 the Kconfig files 874 """ 875 undef_ref_warnings = "\n\n\n".join(warning for warning in kconf.warnings 876 if "undefined symbol" in warning) 877 878 if undef_ref_warnings: 879 self.failure(f"Undefined Kconfig symbols:\n\n {undef_ref_warnings}") 880 881 def check_soc_name_sync(self, kconf): 882 root_args = argparse.Namespace(**{'soc_roots': [Path(ZEPHYR_BASE)]}) 883 v2_systems = list_hardware.find_v2_systems(root_args) 884 885 soc_names = {soc.name for soc in v2_systems.get_socs()} 886 887 soc_kconfig_names = set() 888 for node in kconf.node_iter(): 889 # 'kconfiglib' is global 890 # pylint: disable=undefined-variable 891 if isinstance(node.item, kconfiglib.Symbol) and node.item.name == "SOC": 892 n = node.item 893 for d in n.defaults: 894 soc_kconfig_names.add(d[0].name) 895 896 soc_name_warnings = [] 897 for name in soc_names: 898 if name not in soc_kconfig_names: 899 soc_name_warnings.append(f"soc name: {name} not found in CONFIG_SOC defaults.") 900 901 if soc_name_warnings: 902 soc_name_warning_str = '\n'.join(soc_name_warnings) 903 self.failure(f''' 904Missing SoC names or CONFIG_SOC vs soc.yml out of sync: 905 906{soc_name_warning_str} 907''') 908 909 def check_no_undef_outside_kconfig(self, kconf): 910 """ 911 Checks that there are no references to undefined Kconfig symbols 912 outside Kconfig files (any CONFIG_FOO where no FOO symbol exists) 913 """ 914 # Grep for symbol references. 915 # 916 # Example output line for a reference to CONFIG_FOO at line 17 of 917 # foo/bar.c: 918 # 919 # foo/bar.c<null>17<null>#ifdef CONFIG_FOO 920 # 921 # 'git grep --only-matching' would get rid of the surrounding context 922 # ('#ifdef '), but it was added fairly recently (second half of 2018), 923 # so we extract the references from each line ourselves instead. 924 # 925 # The regex uses word boundaries (\b) to isolate the reference, and 926 # negative lookahead to automatically allowlist the following: 927 # 928 # - ##, for token pasting (CONFIG_FOO_##X) 929 # 930 # - $, e.g. for CMake variable expansion (CONFIG_FOO_${VAR}) 931 # 932 # - @, e.g. for CMakes's configure_file() (CONFIG_FOO_@VAR@) 933 # 934 # - {, e.g. for Python scripts ("CONFIG_FOO_{}_BAR".format(...)") 935 # 936 # - *, meant for comments like '#endif /* CONFIG_FOO_* */ 937 938 defined_syms = self.get_defined_syms(kconf) 939 940 # Maps each undefined symbol to a list <filename>:<linenr> strings 941 undef_to_locs = collections.defaultdict(list) 942 943 # Warning: Needs to work with both --perl-regexp and the 're' module 944 regex = r"\b" + self.CONFIG_ + r"[A-Z0-9_]+\b(?!\s*##|[$@{(.*])" 945 946 # Skip doc/releases and doc/security/vulnerabilities.rst, which often 947 # reference removed symbols 948 grep_stdout = git("grep", "--line-number", "-I", "--null", 949 "--perl-regexp", regex, "--", ":!/doc/releases", 950 ":!/doc/security/vulnerabilities.rst", 951 cwd=Path(GIT_TOP)) 952 953 # splitlines() supports various line terminators 954 for grep_line in grep_stdout.splitlines(): 955 path, lineno, line = grep_line.split("\0") 956 957 # Extract symbol references (might be more than one) within the 958 # line 959 for sym_name in re.findall(regex, line): 960 sym_name = sym_name[len(self.CONFIG_):] # Strip CONFIG_ 961 if sym_name not in defined_syms and \ 962 sym_name not in self.UNDEF_KCONFIG_ALLOWLIST and \ 963 not (sym_name.endswith("_MODULE") and sym_name[:-7] in defined_syms): 964 965 undef_to_locs[sym_name].append(f"{path}:{lineno}") 966 967 if not undef_to_locs: 968 return 969 970 # String that describes all referenced but undefined Kconfig symbols, 971 # in alphabetical order, along with the locations where they're 972 # referenced. Example: 973 # 974 # CONFIG_ALSO_MISSING arch/xtensa/core/fatal.c:273 975 # CONFIG_MISSING arch/xtensa/core/fatal.c:264, subsys/fb/cfb.c:20 976 undef_desc = "\n".join(f"{self.CONFIG_}{sym_name:35} {', '.join(locs)}" 977 for sym_name, locs in sorted(undef_to_locs.items())) 978 979 self.failure(f""" 980Found references to undefined Kconfig symbols. If any of these are false 981positives, then add them to UNDEF_KCONFIG_ALLOWLIST in {__file__}. 982 983If the reference is for a comment like /* CONFIG_FOO_* */ (or 984/* CONFIG_FOO_*_... */), then please use exactly that form (with the '*'). The 985CI check knows not to flag it. 986 987More generally, a reference followed by $, @, {{, (, ., *, or ## will never be 988flagged. 989 990{undef_desc}""") 991 992 # Many of these are symbols used as examples. Note that the list is sorted 993 # alphabetically, and skips the CONFIG_ prefix. 994 UNDEF_KCONFIG_ALLOWLIST = { 995 # zephyr-keep-sorted-start re(^\s+") 996 "ALSO_MISSING", 997 "APP_LINK_WITH_", 998 "APP_LOG_LEVEL", # Application log level is not detected correctly as 999 # the option is defined using a template, so it can't 1000 # be grepped 1001 "APP_LOG_LEVEL_DBG", 1002 "ARMCLANG_STD_LIBC", # The ARMCLANG_STD_LIBC is defined in the 1003 # toolchain Kconfig which is sourced based on 1004 # Zephyr toolchain variant and therefore not 1005 # visible to compliance. 1006 "BINDESC_", # Used in documentation as a prefix 1007 "BOARD_", # Used as regex in scripts/utils/board_v1_to_v2.py 1008 "BOARD_MPS2_AN521_CPUTEST", # Used for board and SoC extension feature tests 1009 "BOARD_NATIVE_SIM_NATIVE_64_TWO", # Used for board and SoC extension feature tests 1010 "BOARD_NATIVE_SIM_NATIVE_ONE", # Used for board and SoC extension feature tests 1011 "BOOT_DIRECT_XIP", # Used in sysbuild for MCUboot configuration 1012 "BOOT_DIRECT_XIP_REVERT", # Used in sysbuild for MCUboot configuration 1013 "BOOT_ENCRYPTION_KEY_FILE", # Used in sysbuild 1014 "BOOT_ENCRYPT_IMAGE", # Used in sysbuild 1015 "BOOT_FIRMWARE_LOADER", # Used in sysbuild for MCUboot configuration 1016 "BOOT_MAX_IMG_SECTORS_AUTO", # Used in sysbuild 1017 "BOOT_RAM_LOAD", # Used in sysbuild for MCUboot configuration 1018 "BOOT_SERIAL_BOOT_MODE", # Used in (sysbuild-based) test/ 1019 # documentation 1020 "BOOT_SERIAL_CDC_ACM", # Used in (sysbuild-based) test 1021 "BOOT_SERIAL_ENTRANCE_GPIO", # Used in (sysbuild-based) test 1022 "BOOT_SERIAL_IMG_GRP_HASH", # Used in documentation 1023 "BOOT_SHARE_BACKEND_RETENTION", # Used in Kconfig text 1024 "BOOT_SHARE_DATA", # Used in Kconfig text 1025 "BOOT_SHARE_DATA_BOOTINFO", # Used in (sysbuild-based) test 1026 "BOOT_SIGNATURE_KEY_FILE", # MCUboot setting used by sysbuild 1027 "BOOT_SIGNATURE_TYPE_ECDSA_P256", # MCUboot setting used by sysbuild 1028 "BOOT_SIGNATURE_TYPE_ED25519", # MCUboot setting used by sysbuild 1029 "BOOT_SIGNATURE_TYPE_NONE", # MCUboot setting used by sysbuild 1030 "BOOT_SIGNATURE_TYPE_RSA", # MCUboot setting used by sysbuild 1031 "BOOT_SWAP_USING_MOVE", # Used in sysbuild for MCUboot configuration 1032 "BOOT_SWAP_USING_OFFSET", # Used in sysbuild for MCUboot configuration 1033 "BOOT_SWAP_USING_SCRATCH", # Used in sysbuild for MCUboot configuration 1034 "BOOT_UPGRADE_ONLY", # Used in example adjusting MCUboot config, but 1035 # symbol is defined in MCUboot itself. 1036 "BOOT_VALIDATE_SLOT0", # Used in (sysbuild-based) test 1037 "BOOT_WATCHDOG_FEED", # Used in (sysbuild-based) test 1038 "BT_6LOWPAN", # Defined in Linux, mentioned in docs 1039 "CDC_ACM_PORT_NAME_", 1040 "CHRE", # Optional module 1041 "CHRE_LOG_LEVEL_DBG", # Optional module 1042 "CLOCK_STM32_SYSCLK_SRC_", 1043 "CMD_CACHE", # Defined in U-Boot, mentioned in docs 1044 "CMU", 1045 "COMPILER_RT_RTLIB", 1046 "CRC", # Used in TI CC13x2 / CC26x2 SDK comment 1047 "DEEP_SLEEP", # #defined by RV32M1 in ext/ 1048 "DESCRIPTION", 1049 "ERR", 1050 "ESP_DIF_LIBRARY", # Referenced in CMake comment 1051 "EXPERIMENTAL", 1052 "EXTRA_FIRMWARE_DIR", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 1053 "FFT", # Used as an example in cmake/extensions.cmake 1054 "FLAG", # Used as an example 1055 "FOO", 1056 "FOO_LOG_LEVEL", 1057 "FOO_SETTING_1", 1058 "FOO_SETTING_2", 1059 "HEAP_MEM_POOL_ADD_SIZE_", # Used as an option matching prefix 1060 "HUGETLBFS", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 1061 "IAR_BUFFERED_WRITE", 1062 "IAR_LIBCPP", 1063 "IAR_SEMIHOSTING", 1064 "IPC_SERVICE_ICMSG_BOND_NOTIFY_REPEAT_TO_MS", # Used in ICMsg tests for intercompatibility 1065 # with older versions of the ICMsg. 1066 "LIBGCC_RTLIB", 1067 "LLVM_USE_LD", # Both LLVM_USE_* are in cmake/toolchain/llvm/Kconfig 1068 "LLVM_USE_LLD", # which are only included if LLVM is selected but 1069 # not other toolchains. Compliance check would complain, 1070 # for example, if you are using GCC. 1071 "LOG_BACKEND_MOCK_OUTPUT_DEFAULT", #Referenced in tests/subsys/logging/log_syst 1072 "LOG_BACKEND_MOCK_OUTPUT_SYST", #Referenced in testcase.yaml of log_syst test 1073 "LSM6DSO_INT_PIN", 1074 "MCUBOOT_ACTION_HOOKS", # Used in (sysbuild-based) test 1075 "MCUBOOT_CLEANUP_ARM_CORE", # Used in (sysbuild-based) test 1076 "MCUBOOT_DOWNGRADE_PREVENTION", # but symbols are defined in MCUboot 1077 # itself. 1078 "MCUBOOT_LOG_LEVEL_INF", 1079 "MCUBOOT_LOG_LEVEL_WRN", # Used in example adjusting MCUboot 1080 # config, 1081 "MCUBOOT_SERIAL", # Used in (sysbuild-based) test/ 1082 # documentation 1083 "MCUMGR_GRP_EXAMPLE_OTHER_HOOK", # Used in documentation 1084 "MISSING", 1085 "MODULES", 1086 "MODVERSIONS", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 1087 "MYFEATURE", 1088 "MY_DRIVER_0", 1089 "NORMAL_SLEEP", # #defined by RV32M1 in ext/ 1090 "NRF_WIFI_FW_BIN", # Directly passed from CMakeLists.txt 1091 "OPT", 1092 "OPT_0", 1093 "PEDO_THS_MIN", 1094 "PSA_H", # This is used in config-psa.h as guard for the header file 1095 "REG1", 1096 "REG2", 1097 "RIMAGE_SIGNING_SCHEMA", # Optional module 1098 "SECURITY_LOADPIN", # Linux, in boards/xtensa/intel_adsp_cavs25/doc 1099 "SEL", 1100 "SHIFT", 1101 "SINGLE_APPLICATION_SLOT", # Used in sysbuild for MCUboot configuration 1102 "SOC_SERIES_", # Used as regex in scripts/utils/board_v1_to_v2.py 1103 "SOC_WATCH", # Issue 13749 1104 "SOME_BOOL", 1105 "SOME_INT", 1106 "SOME_OTHER_BOOL", 1107 "SOME_STRING", 1108 "SRAM2", # Referenced in a comment in samples/application_development 1109 "STACK_SIZE", # Used as an example in the Kconfig docs 1110 "STD_CPP", # Referenced in CMake comment 1111 "TEST1", 1112 "TOOLCHAIN_ARCMWDT_SUPPORTS_THREAD_LOCAL_STORAGE", # The symbol is defined in the toolchain 1113 # Kconfig which is sourced based on Zephyr 1114 # toolchain variant and therefore not visible 1115 # to compliance. 1116 "TYPE_BOOLEAN", 1117 "USB_CONSOLE", 1118 "USE_STDC_", 1119 "WHATEVER", 1120 "ZEPHYR_TRY_MASS_ERASE", # MCUBoot setting described in sysbuild 1121 # documentation 1122 "ZTEST_FAIL_TEST_", # regex in tests/ztest/fail/CMakeLists.txt 1123 # zephyr-keep-sorted-stop 1124 } 1125 1126 1127class KconfigBasicCheck(KconfigCheck): 1128 """ 1129 Checks if we are introducing any new warnings/errors with Kconfig, 1130 for example using undefined Kconfig variables. 1131 This runs the basic Kconfig test, which is checking only for undefined 1132 references inside the Kconfig tree. 1133 """ 1134 name = "KconfigBasic" 1135 1136 def check_no_undef_outside_kconfig(self, kconf): 1137 pass 1138 1139 1140class KconfigBasicNoModulesCheck(KconfigBasicCheck): 1141 """ 1142 Checks if we are introducing any new warnings/errors with Kconfig when no 1143 modules are available. Catches symbols used in the main repository but 1144 defined only in a module. 1145 """ 1146 name = "KconfigBasicNoModules" 1147 1148 def get_modules(self, modules_file, sysbuild_modules_file, settings_file): 1149 with open(modules_file, 'w') as fp_module_file: 1150 fp_module_file.write("# Empty\n") 1151 1152 with open(sysbuild_modules_file, 'w') as fp_module_file: 1153 fp_module_file.write("# Empty\n") 1154 1155 1156class KconfigHWMv2Check(KconfigBasicCheck): 1157 """ 1158 This runs the Kconfig test for board and SoC v2 scheme. 1159 This check ensures that all symbols inside the v2 scheme is also defined 1160 within the same tree. 1161 This ensures the board and SoC trees are fully self-contained and reusable. 1162 """ 1163 name = "KconfigHWMv2" 1164 1165 # Use dedicated Kconfig board / soc v2 scheme file. 1166 # This file sources only v2 scheme tree. 1167 FILENAME = os.path.join(os.path.dirname(__file__), "Kconfig.board.v2") 1168 1169 1170class SysbuildKconfigCheck(KconfigCheck): 1171 """ 1172 Checks if we are introducing any new warnings/errors with sysbuild Kconfig, 1173 for example using undefined Kconfig variables. 1174 """ 1175 name = "SysbuildKconfig" 1176 1177 FILENAME = "share/sysbuild/Kconfig" 1178 CONFIG_ = "SB_CONFIG_" 1179 1180 # A different allowlist is used for symbols prefixed with SB_CONFIG_ (omitted here). 1181 UNDEF_KCONFIG_ALLOWLIST = { 1182 # zephyr-keep-sorted-start re(^\s+") 1183 "FOO", 1184 "MY_IMAGE", # Used in sysbuild documentation as example 1185 "OTHER_APP_IMAGE_NAME", # Used in sysbuild documentation as example 1186 "OTHER_APP_IMAGE_PATH", # Used in sysbuild documentation as example 1187 "SECOND_SAMPLE", # Used in sysbuild documentation 1188 "SUIT_ENVELOPE", # Used by nRF runners to program provisioning data 1189 "SUIT_MPI_APP_AREA_PATH", # Used by nRF runners to program provisioning data 1190 "SUIT_MPI_GENERATE", # Used by nRF runners to program provisioning data 1191 "SUIT_MPI_RAD_AREA_PATH", # Used by nRF runners to program provisioning data 1192 # zephyr-keep-sorted-stop 1193 } 1194 1195 1196class SysbuildKconfigBasicCheck(SysbuildKconfigCheck, KconfigBasicCheck): 1197 """ 1198 Checks if we are introducing any new warnings/errors with sysbuild Kconfig, 1199 for example using undefined Kconfig variables. 1200 This runs the basic Kconfig test, which is checking only for undefined 1201 references inside the sysbuild Kconfig tree. 1202 """ 1203 name = "SysbuildKconfigBasic" 1204 1205 1206class SysbuildKconfigBasicNoModulesCheck(SysbuildKconfigCheck, KconfigBasicNoModulesCheck): 1207 """ 1208 Checks if we are introducing any new warnings/errors with sysbuild Kconfig 1209 when no modules are available. Catches symbols used in the main repository 1210 but defined only in a module. 1211 """ 1212 name = "SysbuildKconfigBasicNoModules" 1213 1214 1215class Nits(ComplianceTest): 1216 """ 1217 Checks various nits in added/modified files. Doesn't check stuff that's 1218 already covered by e.g. checkpatch.pl and pylint. 1219 """ 1220 name = "Nits" 1221 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#coding-style for more details." 1222 path_hint = "<git-top>" 1223 1224 def run(self): 1225 # Loop through added/modified files 1226 for fname in get_files(filter="d"): 1227 if "Kconfig" in fname: 1228 self.check_kconfig_header(fname) 1229 self.check_redundant_zephyr_source(fname) 1230 1231 if fname.startswith("dts/bindings/"): 1232 self.check_redundant_document_separator(fname) 1233 1234 if fname.endswith((".c", ".conf", ".cpp", ".dts", ".overlay", 1235 ".h", ".ld", ".py", ".rst", ".txt", ".yaml", 1236 ".yml")) or \ 1237 "Kconfig" in fname or \ 1238 "defconfig" in fname or \ 1239 fname == "README": 1240 1241 self.check_source_file(fname) 1242 1243 def check_kconfig_header(self, fname): 1244 # Checks for a spammy copy-pasted header format 1245 1246 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 1247 contents = f.read() 1248 1249 # 'Kconfig - yada yada' has a copy-pasted redundant filename at the 1250 # top. This probably means all of the header was copy-pasted. 1251 if re.match(r"\s*#\s*(K|k)config[\w.-]*\s*-", contents): 1252 self.failure(f""" 1253Please use this format for the header in '{fname}' (see 1254https://docs.zephyrproject.org/latest/build/kconfig/tips.html#header-comments-and-other-nits): 1255 1256 # <Overview of symbols defined in the file, preferably in plain English> 1257 (Blank line) 1258 # Copyright (c) 2019 ... 1259 # SPDX-License-Identifier: <License> 1260 (Blank line) 1261 (Kconfig definitions) 1262 1263Skip the "Kconfig - " part of the first line, since it's clear that the comment 1264is about Kconfig from context. The "# Kconfig - " is what triggers this 1265failure. 1266""") 1267 1268 def check_redundant_zephyr_source(self, fname): 1269 # Checks for 'source "$(ZEPHYR_BASE)/Kconfig[.zephyr]"', which can be 1270 # be simplified to 'source "Kconfig[.zephyr]"' 1271 1272 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 1273 # Look for e.g. rsource as well, for completeness 1274 match = re.search( 1275 r'^\s*(?:o|r|or)?source\s*"\$\(?ZEPHYR_BASE\)?/(Kconfig(?:\.zephyr)?)"', 1276 f.read(), re.MULTILINE) 1277 1278 if match: 1279 self.failure(""" 1280Redundant 'source "$(ZEPHYR_BASE)/{0}" in '{1}'. Just do 'source "{0}"' 1281instead. The $srctree environment variable already points to the Zephyr root, 1282and all 'source's are relative to it.""".format(match.group(1), fname)) 1283 1284 def check_redundant_document_separator(self, fname): 1285 # Looks for redundant '...' document separators in bindings 1286 1287 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 1288 if re.search(r"^\.\.\.", f.read(), re.MULTILINE): 1289 self.failure(f"""\ 1290Redundant '...' document separator in {fname}. Binding YAML files are never 1291concatenated together, so no document separators are needed.""") 1292 1293 def check_source_file(self, fname): 1294 # Generic nits related to various source files 1295 1296 with open(os.path.join(GIT_TOP, fname), encoding="utf-8") as f: 1297 contents = f.read() 1298 1299 if not contents.endswith("\n"): 1300 self.failure(f"Missing newline at end of '{fname}'. Check your text " 1301 f"editor settings.") 1302 1303 if contents.startswith("\n"): 1304 self.failure(f"Please remove blank lines at start of '{fname}'") 1305 1306 if contents.endswith("\n\n"): 1307 self.failure(f"Please remove blank lines at end of '{fname}'") 1308 1309 1310class GitDiffCheck(ComplianceTest): 1311 """ 1312 Checks for conflict markers or whitespace errors with git diff --check 1313 """ 1314 name = "GitDiffCheck" 1315 doc = "Git conflict markers and whitespace errors are not allowed in added changes" 1316 path_hint = "<git-top>" 1317 1318 def run(self): 1319 offending_lines = [] 1320 # Use regex to filter out unnecessay output 1321 # Reason: `--check` is mutually exclusive with `--name-only` and `-s` 1322 p = re.compile(r"\S+\: .*\.") 1323 1324 for shaidx in get_shas(COMMIT_RANGE): 1325 # Ignore non-zero return status code 1326 # Reason: `git diff --check` sets the return code to the number of offending lines 1327 diff = git("diff", f"{shaidx}^!", "--check", ignore_non_zero=True) 1328 1329 lines = p.findall(diff) 1330 lines = map(lambda x: f"{shaidx}: {x}", lines) 1331 offending_lines.extend(lines) 1332 1333 if len(offending_lines) > 0: 1334 self.failure("\n".join(offending_lines)) 1335 1336 1337class GitLint(ComplianceTest): 1338 """ 1339 Runs gitlint on the commits and finds issues with style and syntax 1340 1341 """ 1342 name = "Gitlint" 1343 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-guidelines for more details" 1344 path_hint = "<git-top>" 1345 1346 def run(self): 1347 # By default gitlint looks for .gitlint configuration only in 1348 # the current directory 1349 try: 1350 subprocess.run('gitlint --commits ' + COMMIT_RANGE, 1351 check=True, 1352 stdout=subprocess.PIPE, 1353 stderr=subprocess.STDOUT, 1354 shell=True, cwd=GIT_TOP) 1355 1356 except subprocess.CalledProcessError as ex: 1357 self.failure(ex.output.decode("utf-8")) 1358 1359 1360class PyLint(ComplianceTest): 1361 """ 1362 Runs pylint on all .py files, with a limited set of checks enabled. The 1363 configuration is in the pylintrc file. 1364 """ 1365 name = "Pylint" 1366 doc = "See https://www.pylint.org/ for more details" 1367 path_hint = "<git-top>" 1368 1369 def run(self): 1370 # Path to pylint configuration file 1371 pylintrc = os.path.abspath(os.path.join(os.path.dirname(__file__), 1372 "pylintrc")) 1373 1374 # Path to additional pylint check scripts 1375 check_script_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), 1376 "../pylint/checkers")) 1377 1378 # List of files added/modified by the commit(s). 1379 files = get_files(filter="d") 1380 1381 # Filter out everything but Python files. Keep filenames 1382 # relative (to GIT_TOP) to stay farther from any command line 1383 # limit. 1384 py_files = filter_py(GIT_TOP, files) 1385 if not py_files: 1386 return 1387 1388 python_environment = os.environ.copy() 1389 if "PYTHONPATH" in python_environment: 1390 python_environment["PYTHONPATH"] = check_script_dir + ":" + \ 1391 python_environment["PYTHONPATH"] 1392 else: 1393 python_environment["PYTHONPATH"] = check_script_dir 1394 1395 pylintcmd = ["pylint", "--output-format=json2", "--rcfile=" + pylintrc, 1396 "--load-plugins=argparse-checker"] + py_files 1397 logger.info(cmd2str(pylintcmd)) 1398 try: 1399 subprocess.run(pylintcmd, 1400 check=True, 1401 stdout=subprocess.PIPE, 1402 stderr=subprocess.STDOUT, 1403 cwd=GIT_TOP, 1404 env=python_environment) 1405 except subprocess.CalledProcessError as ex: 1406 output = ex.output.decode("utf-8") 1407 messages = json.loads(output)['messages'] 1408 for m in messages: 1409 severity = 'unknown' 1410 if m['messageId'][0] in ('F', 'E'): 1411 severity = 'error' 1412 elif m['messageId'][0] in ('W','C', 'R', 'I'): 1413 severity = 'warning' 1414 self.fmtd_failure(severity, m['messageId'], m['path'], 1415 m['line'], col=str(m['column']), desc=m['message'] 1416 + f" ({m['symbol']})") 1417 1418 if len(messages) == 0: 1419 # If there are no specific messages add the whole output as a failure 1420 self.failure(output) 1421 1422 1423def filter_py(root, fnames): 1424 # PyLint check helper. Returns all Python script filenames among the 1425 # filenames in 'fnames', relative to directory 'root'. 1426 # 1427 # Uses the python-magic library, so that we can detect Python 1428 # files that don't end in .py as well. python-magic is a frontend 1429 # to libmagic, which is also used by 'file'. 1430 return [fname for fname in fnames 1431 if (fname.endswith(".py") or 1432 magic.from_file(os.path.join(root, fname), 1433 mime=True) == "text/x-python")] 1434 1435 1436class Identity(ComplianceTest): 1437 """ 1438 Checks if Emails of author and signed-off messages are consistent. 1439 """ 1440 name = "Identity" 1441 doc = "See https://docs.zephyrproject.org/latest/contribute/guidelines.html#commit-guidelines for more details" 1442 # git rev-list and git log don't depend on the current (sub)directory 1443 # unless explicited 1444 path_hint = "<git-top>" 1445 1446 def run(self): 1447 for shaidx in get_shas(COMMIT_RANGE): 1448 auth_name, auth_email, body = git( 1449 'show', '-s', '--format=%an%n%ae%n%b', shaidx 1450 ).split('\n', 2) 1451 1452 match_signoff = re.search(r"signed-off-by:\s(.*)", body, 1453 re.IGNORECASE) 1454 detailed_match = re.search(r"signed-off-by:\s(.*) <(.*)>", body, 1455 re.IGNORECASE) 1456 1457 failures = [] 1458 1459 if auth_email.endswith("@users.noreply.github.com"): 1460 failures.append(f"{shaidx}: author email ({auth_email}) must " 1461 "be a real email and cannot end in " 1462 "@users.noreply.github.com") 1463 1464 if not match_signoff: 1465 failures.append(f'{shaidx}: Missing signed-off-by line') 1466 elif not detailed_match: 1467 signoff = match_signoff.group(0) 1468 failures.append(f"{shaidx}: Signed-off-by line ({signoff}) " 1469 "does not follow the syntax: First " 1470 "Last <email>.") 1471 elif (auth_name, auth_email) != detailed_match.groups(): 1472 failures.append(f"{shaidx}: author email ({auth_email}) needs " 1473 "to match one of the signed-off-by entries.") 1474 1475 if failures: 1476 self.failure('\n'.join(failures)) 1477 1478 1479class BinaryFiles(ComplianceTest): 1480 """ 1481 Check that the diff contains no binary files. 1482 """ 1483 name = "BinaryFiles" 1484 doc = "No binary files allowed." 1485 path_hint = "<git-top>" 1486 1487 def run(self): 1488 BINARY_ALLOW_PATHS = ("doc/", "boards/", "samples/") 1489 # svg files are always detected as binary, see .gitattributes 1490 BINARY_ALLOW_EXT = (".jpg", ".jpeg", ".png", ".svg", ".webp") 1491 1492 for stat in git("diff", "--numstat", "--diff-filter=A", 1493 COMMIT_RANGE).splitlines(): 1494 added, deleted, fname = stat.split("\t") 1495 if added == "-" and deleted == "-": 1496 if (fname.startswith(BINARY_ALLOW_PATHS) and 1497 fname.endswith(BINARY_ALLOW_EXT)): 1498 continue 1499 self.failure(f"Binary file not allowed: {fname}") 1500 1501 1502class ImageSize(ComplianceTest): 1503 """ 1504 Check that any added image is limited in size. 1505 """ 1506 name = "ImageSize" 1507 doc = "Check the size of image files." 1508 path_hint = "<git-top>" 1509 1510 def run(self): 1511 SIZE_LIMIT = 250 << 10 1512 BOARD_SIZE_LIMIT = 100 << 10 1513 1514 for file in get_files(filter="d"): 1515 full_path = os.path.join(GIT_TOP, file) 1516 mime_type = magic.from_file(full_path, mime=True) 1517 1518 if not mime_type.startswith("image/"): 1519 continue 1520 1521 size = os.path.getsize(full_path) 1522 1523 limit = SIZE_LIMIT 1524 if file.startswith("boards/"): 1525 limit = BOARD_SIZE_LIMIT 1526 1527 if size > limit: 1528 self.failure(f"Image file too large: {file} reduce size to " 1529 f"less than {limit >> 10}kB") 1530 1531 1532class MaintainersFormat(ComplianceTest): 1533 """ 1534 Check that MAINTAINERS file parses correctly. 1535 """ 1536 name = "MaintainersFormat" 1537 doc = "Check that MAINTAINERS file parses correctly." 1538 path_hint = "<git-top>" 1539 1540 def run(self): 1541 MAINTAINERS_FILES = ["MAINTAINERS.yml", "MAINTAINERS.yaml"] 1542 1543 for file in MAINTAINERS_FILES: 1544 if not os.path.exists(file): 1545 continue 1546 1547 try: 1548 Maintainers(file) 1549 except MaintainersError as ex: 1550 self.failure(f"Error parsing {file}: {ex}") 1551 1552class ModulesMaintainers(ComplianceTest): 1553 """ 1554 Check that all modules have a MAINTAINERS entry. 1555 """ 1556 name = "ModulesMaintainers" 1557 doc = "Check that all modules have a MAINTAINERS entry." 1558 path_hint = "<git-top>" 1559 1560 def run(self): 1561 MAINTAINERS_FILES = ["MAINTAINERS.yml", "MAINTAINERS.yaml"] 1562 1563 manifest = Manifest.from_file() 1564 1565 maintainers_file = None 1566 for file in MAINTAINERS_FILES: 1567 if os.path.exists(file): 1568 maintainers_file = file 1569 break 1570 if not maintainers_file: 1571 return 1572 1573 maintainers = Maintainers(maintainers_file) 1574 1575 for project in manifest.get_projects([]): 1576 if not manifest.is_active(project): 1577 continue 1578 1579 if isinstance(project, ManifestProject): 1580 continue 1581 1582 area = f"West project: {project.name}" 1583 if area not in maintainers.areas: 1584 self.failure(f"Missing {maintainers_file} entry for: \"{area}\"") 1585 1586 1587class YAMLLint(ComplianceTest): 1588 """ 1589 YAMLLint 1590 """ 1591 name = "YAMLLint" 1592 doc = "Check YAML files with YAMLLint." 1593 path_hint = "<git-top>" 1594 1595 def run(self): 1596 config_file = os.path.join(ZEPHYR_BASE, ".yamllint") 1597 1598 for file in get_files(filter="d"): 1599 if Path(file).suffix not in ['.yaml', '.yml']: 1600 continue 1601 1602 yaml_config = config.YamlLintConfig(file=config_file) 1603 1604 if file.startswith(".github/"): 1605 # Tweak few rules for workflow files. 1606 yaml_config.rules["line-length"] = False 1607 yaml_config.rules["truthy"]["allowed-values"].extend(['on', 'off']) 1608 elif file == ".codecov.yml": 1609 yaml_config.rules["truthy"]["allowed-values"].extend(['yes', 'no']) 1610 1611 with open(file, 'r') as fp: 1612 for p in linter.run(fp, yaml_config): 1613 self.fmtd_failure('warning', f'YAMLLint ({p.rule})', file, 1614 p.line, col=p.column, desc=p.desc) 1615 1616 1617class SphinxLint(ComplianceTest): 1618 """ 1619 SphinxLint 1620 """ 1621 1622 name = "SphinxLint" 1623 doc = "Check Sphinx/reStructuredText files with sphinx-lint." 1624 path_hint = "<git-top>" 1625 1626 # Checkers added/removed to sphinx-lint's default set 1627 DISABLE_CHECKERS = ["horizontal-tab", "missing-space-before-default-role"] 1628 ENABLE_CHECKERS = ["default-role"] 1629 1630 def run(self): 1631 for file in get_files(): 1632 if not file.endswith(".rst"): 1633 continue 1634 1635 try: 1636 # sphinx-lint does not expose a public API so interaction is done via CLI 1637 subprocess.run( 1638 f"sphinx-lint -d {','.join(self.DISABLE_CHECKERS)} -e {','.join(self.ENABLE_CHECKERS)} {file}", 1639 check=True, 1640 stdout=subprocess.PIPE, 1641 stderr=subprocess.STDOUT, 1642 shell=True, 1643 cwd=GIT_TOP, 1644 ) 1645 1646 except subprocess.CalledProcessError as ex: 1647 for line in ex.output.decode("utf-8").splitlines(): 1648 match = re.match(r"^(.*):(\d+): (.*)$", line) 1649 1650 if match: 1651 self.fmtd_failure( 1652 "error", 1653 "SphinxLint", 1654 match.group(1), 1655 int(match.group(2)), 1656 desc=match.group(3), 1657 ) 1658 1659 1660class KeepSorted(ComplianceTest): 1661 """ 1662 Check for blocks of code or config that should be kept sorted. 1663 """ 1664 name = "KeepSorted" 1665 doc = "Check for blocks of code or config that should be kept sorted." 1666 path_hint = "<git-top>" 1667 1668 MARKER = "zephyr-keep-sorted" 1669 1670 def block_check_sorted(self, block_data, regex): 1671 def _test_indent(txt: str): 1672 return txt.startswith((" ", "\t")) 1673 1674 if regex is None: 1675 block_data = textwrap.dedent(block_data) 1676 1677 lines = block_data.splitlines() 1678 last = '' 1679 1680 for idx, line in enumerate(lines): 1681 if not line.strip(): 1682 # Ignore blank lines 1683 continue 1684 1685 if regex: 1686 # check for regex 1687 if not re.match(regex, line): 1688 continue 1689 else: 1690 if _test_indent(line): 1691 continue 1692 1693 # Fold back indented lines after the current one 1694 for cont in takewhile(_test_indent, lines[idx + 1:]): 1695 line += cont.strip() 1696 1697 if line < last: 1698 return idx 1699 1700 last = line 1701 1702 return -1 1703 1704 def check_file(self, file, fp): 1705 mime_type = magic.from_file(file, mime=True) 1706 1707 if not mime_type.startswith("text/"): 1708 return 1709 1710 block_data = "" 1711 in_block = False 1712 1713 start_marker = f"{self.MARKER}-start" 1714 stop_marker = f"{self.MARKER}-stop" 1715 regex_marker = r"re\((.+)\)" 1716 start_line = 0 1717 regex = None 1718 1719 for line_num, line in enumerate(fp.readlines(), start=1): 1720 if start_marker in line: 1721 if in_block: 1722 desc = f"nested {start_marker}" 1723 self.fmtd_failure("error", "KeepSorted", file, line_num, 1724 desc=desc) 1725 in_block = True 1726 block_data = "" 1727 start_line = line_num + 1 1728 1729 # Test for a regex block 1730 match = re.search(regex_marker, line) 1731 regex = match.group(1) if match else None 1732 elif stop_marker in line: 1733 if not in_block: 1734 desc = f"{stop_marker} without {start_marker}" 1735 self.fmtd_failure("error", "KeepSorted", file, line_num, 1736 desc=desc) 1737 in_block = False 1738 1739 idx = self.block_check_sorted(block_data, regex) 1740 if idx >= 0: 1741 desc = f"sorted block has out-of-order line at {start_line + idx}" 1742 self.fmtd_failure("error", "KeepSorted", file, line_num, 1743 desc=desc) 1744 elif in_block: 1745 block_data += line 1746 1747 if in_block: 1748 self.failure(f"unterminated {start_marker} in {file}") 1749 1750 def run(self): 1751 for file in get_files(filter="d"): 1752 with open(file, "r") as fp: 1753 self.check_file(file, fp) 1754 1755 1756class Ruff(ComplianceTest): 1757 """ 1758 Ruff 1759 """ 1760 name = "Ruff" 1761 doc = "Check python files with ruff." 1762 path_hint = "<git-top>" 1763 1764 def run(self): 1765 for file in get_files(filter="d"): 1766 if not file.endswith(".py"): 1767 continue 1768 1769 try: 1770 subprocess.run( 1771 f"ruff check --force-exclude --output-format=json {file}", 1772 check=True, 1773 stdout=subprocess.PIPE, 1774 stderr=subprocess.DEVNULL, 1775 shell=True, 1776 cwd=GIT_TOP, 1777 ) 1778 except subprocess.CalledProcessError as ex: 1779 output = ex.output.decode("utf-8") 1780 messages = json.loads(output) 1781 for m in messages: 1782 self.fmtd_failure( 1783 "error", 1784 f'Python lint error ({m.get("code")}) see {m.get("url")}', 1785 file, 1786 line=m.get("location", {}).get("row"), 1787 col=m.get("location", {}).get("column"), 1788 end_line=m.get("end_location", {}).get("row"), 1789 end_col=m.get("end_location", {}).get("column"), 1790 desc=m.get("message"), 1791 ) 1792 try: 1793 subprocess.run( 1794 f"ruff format --force-exclude --diff {file}", 1795 check=True, 1796 shell=True, 1797 cwd=GIT_TOP, 1798 ) 1799 except subprocess.CalledProcessError: 1800 desc = f"Run 'ruff format {file}'" 1801 self.fmtd_failure("error", "Python format error", file, desc=desc) 1802 1803 1804class TextEncoding(ComplianceTest): 1805 """ 1806 Check that any text file is encoded in ascii or utf-8. 1807 """ 1808 name = "TextEncoding" 1809 doc = "Check the encoding of text files." 1810 path_hint = "<git-top>" 1811 1812 ALLOWED_CHARSETS = ["us-ascii", "utf-8"] 1813 1814 def run(self): 1815 m = magic.Magic(mime=True, mime_encoding=True) 1816 1817 for file in get_files(filter="d"): 1818 full_path = os.path.join(GIT_TOP, file) 1819 mime_type = m.from_file(full_path) 1820 1821 if not mime_type.startswith("text/"): 1822 continue 1823 1824 # format is "text/<type>; charset=<charset>" 1825 if mime_type.rsplit('=')[-1] not in self.ALLOWED_CHARSETS: 1826 desc = f"Text file with unsupported encoding: {file} has mime type {mime_type}" 1827 self.fmtd_failure("error", "TextEncoding", file, desc=desc) 1828 1829 1830def init_logs(cli_arg): 1831 # Initializes logging 1832 1833 global logger 1834 1835 level = os.environ.get('LOG_LEVEL', "WARN") 1836 1837 console = logging.StreamHandler() 1838 console.setFormatter(logging.Formatter('%(levelname)-8s: %(message)s')) 1839 1840 logger = logging.getLogger('') 1841 logger.addHandler(console) 1842 logger.setLevel(cli_arg or level) 1843 1844 logger.info("Log init completed, level=%s", 1845 logging.getLevelName(logger.getEffectiveLevel())) 1846 1847 1848def inheritors(klass): 1849 subclasses = set() 1850 work = [klass] 1851 while work: 1852 parent = work.pop() 1853 for child in parent.__subclasses__(): 1854 if child not in subclasses: 1855 subclasses.add(child) 1856 work.append(child) 1857 return subclasses 1858 1859 1860def annotate(res): 1861 """ 1862 https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#about-workflow-commands 1863 """ 1864 msg = res.message.replace('%', '%25').replace('\n', '%0A').replace('\r', '%0D') 1865 notice = f'::{res.severity} file={res.file}' + \ 1866 (f',line={res.line}' if res.line else '') + \ 1867 (f',col={res.col}' if res.col else '') + \ 1868 (f',endLine={res.end_line}' if res.end_line else '') + \ 1869 (f',endColumn={res.end_col}' if res.end_col else '') + \ 1870 f',title={res.title}::{msg}' 1871 print(notice) 1872 1873 1874def resolve_path_hint(hint): 1875 if hint == "<zephyr-base>": 1876 return ZEPHYR_BASE 1877 elif hint == "<git-top>": 1878 return GIT_TOP 1879 else: 1880 return hint 1881 1882 1883def parse_args(argv): 1884 1885 default_range = 'HEAD~1..HEAD' 1886 parser = argparse.ArgumentParser( 1887 description="Check for coding style and documentation warnings.", allow_abbrev=False) 1888 parser.add_argument('-c', '--commits', default=default_range, 1889 help=f'''Commit range in the form: a..[b], default is 1890 {default_range}''') 1891 parser.add_argument('-o', '--output', default="compliance.xml", 1892 help='''Name of outfile in JUnit format, 1893 default is ./compliance.xml''') 1894 parser.add_argument('-n', '--no-case-output', action="store_true", 1895 help="Do not store the individual test case output.") 1896 parser.add_argument('-l', '--list', action="store_true", 1897 help="List all checks and exit") 1898 parser.add_argument("-v", "--loglevel", choices=['DEBUG', 'INFO', 'WARNING', 1899 'ERROR', 'CRITICAL'], 1900 help="python logging level") 1901 parser.add_argument('-m', '--module', action="append", default=[], 1902 help="Checks to run. All checks by default. (case " \ 1903 "insensitive)") 1904 parser.add_argument('-e', '--exclude-module', action="append", default=[], 1905 help="Do not run the specified checks (case " \ 1906 "insensitive)") 1907 parser.add_argument('-j', '--previous-run', default=None, 1908 help='''Pre-load JUnit results in XML format 1909 from a previous run and combine with new results.''') 1910 parser.add_argument('--annotate', action="store_true", 1911 help="Print GitHub Actions-compatible annotations.") 1912 1913 return parser.parse_args(argv) 1914 1915def _main(args): 1916 # The "real" main(), which is wrapped to catch exceptions and report them 1917 # to GitHub. Returns the number of test failures. 1918 1919 global ZEPHYR_BASE 1920 ZEPHYR_BASE = os.environ.get('ZEPHYR_BASE') 1921 if not ZEPHYR_BASE: 1922 # Let the user run this script as ./scripts/ci/check_compliance.py without 1923 # making them set ZEPHYR_BASE. 1924 ZEPHYR_BASE = str(Path(__file__).resolve().parents[2]) 1925 1926 # Propagate this decision to child processes. 1927 os.environ['ZEPHYR_BASE'] = ZEPHYR_BASE 1928 1929 # The absolute path of the top-level git directory. Initialize it here so 1930 # that issues running Git can be reported to GitHub. 1931 global GIT_TOP 1932 GIT_TOP = git("rev-parse", "--show-toplevel") 1933 1934 # The commit range passed in --commit, e.g. "HEAD~3" 1935 global COMMIT_RANGE 1936 COMMIT_RANGE = args.commits 1937 1938 init_logs(args.loglevel) 1939 1940 logger.info(f'Running tests on commit range {COMMIT_RANGE}') 1941 1942 if args.list: 1943 for testcase in sorted(inheritors(ComplianceTest), key=lambda x: x.name): 1944 print(testcase.name) 1945 return 0 1946 1947 # Load saved test results from an earlier run, if requested 1948 if args.previous_run: 1949 if not os.path.exists(args.previous_run): 1950 # This probably means that an earlier pass had an internal error 1951 # (the script is currently run multiple times by the ci-pipelines 1952 # repo). Since that earlier pass might've posted an error to 1953 # GitHub, avoid generating a GitHub comment here, by avoiding 1954 # sys.exit() (which gets caught in main()). 1955 print(f"error: '{args.previous_run}' not found", 1956 file=sys.stderr) 1957 return 1 1958 1959 logging.info(f"Loading previous results from {args.previous_run}") 1960 for loaded_suite in JUnitXml.fromfile(args.previous_run): 1961 suite = loaded_suite 1962 break 1963 else: 1964 suite = TestSuite("Compliance") 1965 1966 included = list(map(lambda x: x.lower(), args.module)) 1967 excluded = list(map(lambda x: x.lower(), args.exclude_module)) 1968 1969 for testcase in inheritors(ComplianceTest): 1970 # "Modules" and "testcases" are the same thing. Better flags would have 1971 # been --tests and --exclude-tests or the like, but it's awkward to 1972 # change now. 1973 1974 if included and testcase.name.lower() not in included: 1975 continue 1976 1977 if testcase.name.lower() in excluded: 1978 print("Skipping " + testcase.name) 1979 continue 1980 1981 test = testcase() 1982 try: 1983 print(f"Running {test.name:16} tests in " 1984 f"{resolve_path_hint(test.path_hint)} ...") 1985 test.run() 1986 except EndTest: 1987 pass 1988 1989 # Annotate if required 1990 if args.annotate: 1991 for res in test.fmtd_failures: 1992 annotate(res) 1993 1994 suite.add_testcase(test.case) 1995 1996 if args.output: 1997 xml = JUnitXml() 1998 xml.add_testsuite(suite) 1999 xml.update_statistics() 2000 xml.write(args.output, pretty=True) 2001 2002 failed_cases = [] 2003 name2doc = {testcase.name: testcase.doc 2004 for testcase in inheritors(ComplianceTest)} 2005 2006 for case in suite: 2007 if case.result: 2008 if case.is_skipped: 2009 logging.warning(f"Skipped {case.name}") 2010 else: 2011 failed_cases.append(case) 2012 else: 2013 # Some checks can produce no .result 2014 logging.info(f"No JUnit result for {case.name}") 2015 2016 n_fails = len(failed_cases) 2017 2018 if n_fails: 2019 print(f"{n_fails} checks failed") 2020 for case in failed_cases: 2021 for res in case.result: 2022 errmsg = res.text.strip() 2023 logging.error(f"Test {case.name} failed: \n{errmsg}") 2024 if args.no_case_output: 2025 continue 2026 with open(f"{case.name}.txt", "w") as f: 2027 docs = name2doc.get(case.name) 2028 f.write(f"{docs}\n") 2029 for res in case.result: 2030 errmsg = res.text.strip() 2031 f.write(f'\n {errmsg}') 2032 2033 if args.output: 2034 print(f"\nComplete results in {args.output}") 2035 return n_fails 2036 2037 2038def main(argv=None): 2039 args = parse_args(argv) 2040 2041 try: 2042 # pylint: disable=unused-import 2043 from lxml import etree 2044 except ImportError: 2045 print("\nERROR: Python module lxml not installed, unable to proceed") 2046 print("See https://github.com/weiwei/junitparser/issues/99") 2047 return 1 2048 2049 try: 2050 n_fails = _main(args) 2051 except BaseException: 2052 # Catch BaseException instead of Exception to include stuff like 2053 # SystemExit (raised by sys.exit()) 2054 print(f"Python exception in `{__file__}`:\n\n" 2055 f"```\n{traceback.format_exc()}\n```") 2056 2057 raise 2058 2059 sys.exit(n_fails) 2060 2061 2062def cmd2str(cmd): 2063 # Formats the command-line arguments in the iterable 'cmd' into a string, 2064 # for error messages and the like 2065 2066 return " ".join(shlex.quote(word) for word in cmd) 2067 2068 2069def err(msg): 2070 cmd = sys.argv[0] # Empty if missing 2071 if cmd: 2072 cmd += ": " 2073 sys.exit(f"{cmd} error: {msg}") 2074 2075 2076if __name__ == "__main__": 2077 main(sys.argv[1:]) 2078