check_public_headers.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404
  1. #!/usr/bin/env python
  2. #
  3. # Checks all public headers in IDF in the ci
  4. #
  5. # SPDX-FileCopyrightText: 2020-2022 Espressif Systems (Shanghai) CO LTD
  6. # SPDX-License-Identifier: Apache-2.0
  7. #
  8. from __future__ import print_function, unicode_literals
  9. import argparse
  10. import fnmatch
  11. import json
  12. import os
  13. import queue
  14. import re
  15. import subprocess
  16. import tempfile
  17. from io import open
  18. from threading import Event, Thread
  19. from typing import List, Optional, Set, Tuple, Union
  20. class HeaderFailed(Exception):
  21. """Base header failure exeption"""
  22. pass
  23. class HeaderFailedSdkconfig(HeaderFailed):
  24. def __str__(self) -> str:
  25. return 'Sdkconfig Error'
  26. class HeaderFailedBuildError(HeaderFailed):
  27. def __init__(self, compiler: str):
  28. self.compiler = compiler
  29. def __str__(self) -> str:
  30. return 'Header Build Error with {}'.format(self.compiler)
  31. class HeaderFailedPreprocessError(HeaderFailed):
  32. def __str__(self) -> str:
  33. return 'Header Procecessing Error'
  34. class HeaderFailedCppGuardMissing(HeaderFailed):
  35. def __str__(self) -> str:
  36. return 'Header Missing C++ Guard'
  37. class HeaderFailedContainsCode(HeaderFailed):
  38. def __str__(self) -> str:
  39. return 'Header Produced non-zero object'
  40. class HeaderFailedContainsStaticAssert(HeaderFailed):
  41. def __str__(self) -> str:
  42. return 'Header uses _Static_assert or static_assert instead of ESP_STATIC_ASSERT'
  43. # Creates a temp file and returns both output as a string and a file name
  44. #
  45. def exec_cmd_to_temp_file(what: List, suffix: str='') -> Tuple[int, str, str, str, str]:
  46. out_file = tempfile.NamedTemporaryFile(suffix=suffix, delete=False)
  47. rc, out, err, cmd = exec_cmd(what, out_file)
  48. with open(out_file.name, 'r', encoding='utf-8') as f:
  49. out = f.read()
  50. return rc, out, err, out_file.name, cmd
  51. def exec_cmd(what: List, out_file: Union['tempfile._TemporaryFileWrapper[bytes]', int]=subprocess.PIPE) -> Tuple[int, str, str, str]:
  52. p = subprocess.Popen(what, stdin=subprocess.PIPE, stdout=out_file, stderr=subprocess.PIPE)
  53. output_b, err_b = p.communicate()
  54. rc = p.returncode
  55. output: str = output_b.decode('utf-8') if output_b is not None else ''
  56. err: str = err_b.decode('utf-8') if err_b is not None else ''
  57. return rc, output, err, ' '.join(what)
  58. class PublicHeaderChecker:
  59. # Intermediate results
  60. COMPILE_ERR_REF_CONFIG_HDR_FAILED = 1 # -> Cannot compile and failed with injected SDKCONFIG #error (header FAILs)
  61. COMPILE_ERR_ERROR_MACRO_HDR_OK = 2 # -> Cannot compile, but failed with "#error" directive (header seems OK)
  62. COMPILE_ERR_HDR_FAILED = 3 # -> Cannot compile with another issue, logged if verbose (header FAILs)
  63. PREPROC_OUT_ZERO_HDR_OK = 4 # -> Both preprocessors produce zero out (header file is OK)
  64. PREPROC_OUT_SAME_HRD_FAILED = 5 # -> Both preprocessors produce the same, non-zero output (header file FAILs)
  65. PREPROC_OUT_DIFFERENT_WITH_EXT_C_HDR_OK = 6 # -> Both preprocessors produce different, non-zero output with extern "C" (header seems OK)
  66. PREPROC_OUT_DIFFERENT_NO_EXT_C_HDR_FAILED = 7 # -> Both preprocessors produce different, non-zero output without extern "C" (header fails)
  67. HEADER_CONTAINS_STATIC_ASSERT = 8 # -> Header file contains _Static_assert instead of static_assert or ESP_STATIC_ASSERT
  68. def log(self, message: str, debug: bool=False) -> None:
  69. if self.verbose or debug:
  70. print(message)
  71. def __init__(self, verbose: bool=False, jobs: int=1, prefix: Optional[str]=None) -> None:
  72. self.gcc = '{}gcc'.format(prefix)
  73. self.gpp = '{}g++'.format(prefix)
  74. self.verbose = verbose
  75. self.jobs = jobs
  76. self.prefix = prefix
  77. self.extern_c = re.compile(r'extern "C"')
  78. self.error_macro = re.compile(r'#error')
  79. self.error_orphan_kconfig = re.compile(r'#error CONFIG_VARS_USED_WHILE_SDKCONFIG_NOT_INCLUDED')
  80. self.kconfig_macro = re.compile(r'\bCONFIG_[A-Z0-9_]+')
  81. self.static_assert = re.compile(r'(_Static_assert|static_assert)')
  82. self.defines_assert = re.compile(r'#define[ \t]+ESP_STATIC_ASSERT')
  83. self.auto_soc_header = re.compile(r'components/soc/esp[a-z0-9_]+/include(?:/rev[0-9]+)?/(soc|modem)/[a-zA-Z0-9_]+.h')
  84. self.assembly_nocode = r'^\s*(\.file|\.text|\.ident|\.option|\.attribute).*$'
  85. self.check_threads: List[Thread] = []
  86. self.job_queue: queue.Queue = queue.Queue()
  87. self.failed_queue: queue.Queue = queue.Queue()
  88. self.terminate = Event()
  89. def __enter__(self) -> 'PublicHeaderChecker':
  90. for i in range(self.jobs):
  91. t = Thread(target=self.check_headers, args=(i, ))
  92. self.check_threads.append(t)
  93. t.start()
  94. return self
  95. def __exit__(self, exc_type: str, exc_value: str, traceback: str) -> None:
  96. self.terminate.set()
  97. for t in self.check_threads:
  98. t.join()
  99. # thread function process incoming header file from a queue
  100. def check_headers(self, num: int) -> None:
  101. while not self.terminate.is_set():
  102. if not self.job_queue.empty():
  103. task = self.job_queue.get()
  104. if task is None:
  105. self.terminate.set()
  106. else:
  107. try:
  108. self.check_one_header(task, num)
  109. except HeaderFailed as e:
  110. self.failed_queue.put('{}: Failed! {}'.format(task, e))
  111. except Exception as e:
  112. # Makes sure any unexpected exceptions causes the program to terminate
  113. self.failed_queue.put('{}: Failed! {}'.format(task, e))
  114. self.terminate.set()
  115. raise
  116. def get_failed(self) -> List:
  117. return list(self.failed_queue.queue)
  118. def join(self) -> None:
  119. for t in self.check_threads:
  120. while t.is_alive() and not self.terminate.is_set():
  121. t.join(1) # joins with timeout to respond to keyboard interrupt
  122. # Checks one header calling:
  123. # - preprocess_one_header() to test and compare preprocessor outputs
  124. # - check_no_code() to test if header contains some executable code
  125. # Procedure
  126. # 1) Preprocess the include file with C preprocessor and with CPP preprocessor
  127. # - Pass the test if the preprocessor outputs are the same and whitespaces only (#define only header)
  128. # - Fail the test if the preprocessor outputs are the same (but with some code)
  129. # - If outputs different, continue with 2)
  130. # 2) Strip out all include directives to generate "temp.h"
  131. # 3) Preprocess the temp.h the same way in (1)
  132. # - Pass the test if the preprocessor outputs are the same and whitespaces only (#include only header)
  133. # - Fail the test if the preprocessor outputs are the same (but with some code)
  134. # - If outputs different, pass the test
  135. # 4) If header passed the steps 1) and 3) test that it produced zero assembly code
  136. def check_one_header(self, header: str, num: int) -> None:
  137. res = self.preprocess_one_header(header, num)
  138. if res == self.COMPILE_ERR_REF_CONFIG_HDR_FAILED:
  139. raise HeaderFailedSdkconfig()
  140. elif res == self.COMPILE_ERR_ERROR_MACRO_HDR_OK:
  141. return self.compile_one_header(header)
  142. elif res == self.COMPILE_ERR_HDR_FAILED:
  143. raise HeaderFailedPreprocessError()
  144. elif res == self.PREPROC_OUT_ZERO_HDR_OK:
  145. return self.compile_one_header(header)
  146. elif res == self.PREPROC_OUT_SAME_HRD_FAILED:
  147. raise HeaderFailedCppGuardMissing()
  148. elif res == self.HEADER_CONTAINS_STATIC_ASSERT:
  149. raise HeaderFailedContainsStaticAssert()
  150. else:
  151. self.compile_one_header(header)
  152. temp_header = None
  153. try:
  154. _, _, _, temp_header, _ = exec_cmd_to_temp_file(['sed', '/#include/d; /#error/d', header], suffix='.h')
  155. res = self.preprocess_one_header(temp_header, num, ignore_common_issues=True)
  156. if res == self.PREPROC_OUT_SAME_HRD_FAILED:
  157. raise HeaderFailedCppGuardMissing()
  158. elif res == self.PREPROC_OUT_DIFFERENT_NO_EXT_C_HDR_FAILED:
  159. raise HeaderFailedCppGuardMissing()
  160. finally:
  161. if temp_header:
  162. os.unlink(temp_header)
  163. def compile_one_header(self, header: str) -> None:
  164. self.compile_one_header_with(self.gcc, header)
  165. self.compile_one_header_with(self.gpp, header)
  166. def compile_one_header_with(self, compiler: str, header: str) -> None:
  167. rc, out, err, cmd = exec_cmd([compiler, '-S', '-o-', '-include', header, self.main_c] + self.include_dir_flags)
  168. if rc == 0:
  169. if not re.sub(self.assembly_nocode, '', out, flags=re.M).isspace():
  170. raise HeaderFailedContainsCode()
  171. return # Header OK: produced zero code
  172. self.log('{}: FAILED: compilation issue'.format(header), True)
  173. self.log(err, True)
  174. self.log('\nCompilation command failed:\n{}\n'.format(cmd), True)
  175. raise HeaderFailedBuildError(compiler)
  176. def preprocess_one_header(self, header: str, num: int, ignore_common_issues: bool=False) -> int:
  177. all_compilation_flags = ['-w', '-P', '-E', '-DESP_PLATFORM', '-include', header, self.main_c] + self.include_dir_flags
  178. # just strip comments to check for CONFIG_... macros or static asserts
  179. rc, out, err, _ = exec_cmd([self.gcc, '-fpreprocessed', '-dD', '-P', '-E', header] + self.include_dir_flags)
  180. if not ignore_common_issues: # We ignore issues on sdkconfig and static asserts, as we're looking at "preprocessed output"
  181. if re.search(self.kconfig_macro, out):
  182. # enable defined #error if sdkconfig.h not included
  183. all_compilation_flags.append('-DIDF_CHECK_SDKCONFIG_INCLUDED')
  184. # If the file contain _Static_assert or static_assert, make sure it does't not define ESP_STATIC_ASSERT and that it
  185. # is not an automatically generated soc header file
  186. grp = re.search(self.static_assert, out)
  187. # Normalize the potential A//B, A/./B, A/../A, from the name
  188. normalized_path = os.path.normpath(header)
  189. if grp and not re.search(self.defines_assert, out) and not re.search(self.auto_soc_header, normalized_path):
  190. self.log('{}: FAILED: contains {}. Please use ESP_STATIC_ASSERT'.format(header, grp.group(1)), True)
  191. return self.HEADER_CONTAINS_STATIC_ASSERT
  192. try:
  193. # compile with C++, check for errors, outputs for a temp file
  194. rc, cpp_out, err, cpp_out_file, cmd = exec_cmd_to_temp_file([self.gpp, '--std=c++17'] + all_compilation_flags)
  195. if rc != 0:
  196. if re.search(self.error_macro, err):
  197. if re.search(self.error_orphan_kconfig, err):
  198. self.log('{}: CONFIG_VARS_USED_WHILE_SDKCONFIG_NOT_INCLUDED'.format(header), True)
  199. return self.COMPILE_ERR_REF_CONFIG_HDR_FAILED
  200. self.log('{}: Error directive failure: OK'.format(header))
  201. return self.COMPILE_ERR_ERROR_MACRO_HDR_OK
  202. self.log('{}: FAILED: compilation issue'.format(header), True)
  203. self.log(err, True)
  204. self.log('\nCompilation command failed:\n{}\n'.format(cmd), True)
  205. return self.COMPILE_ERR_HDR_FAILED
  206. # compile with C compiler, outputs to another temp file
  207. rc, _, err, c99_out_file, _ = exec_cmd_to_temp_file([self.gcc, '--std=c99'] + all_compilation_flags)
  208. if rc != 0:
  209. self.log('{} FAILED should never happen'.format(header))
  210. return self.COMPILE_ERR_HDR_FAILED
  211. # diff the two outputs
  212. rc, diff, err, _ = exec_cmd(['diff', c99_out_file, cpp_out_file])
  213. if not diff or diff.isspace():
  214. if not cpp_out or cpp_out.isspace():
  215. self.log('{} The same, but empty out - OK'.format(header))
  216. return self.PREPROC_OUT_ZERO_HDR_OK
  217. self.log('{} FAILED C and C++ preprocessor output is the same!'.format(header), True)
  218. return self.PREPROC_OUT_SAME_HRD_FAILED
  219. if re.search(self.extern_c, diff):
  220. self.log('{} extern C present - OK'.format(header))
  221. return self.PREPROC_OUT_DIFFERENT_WITH_EXT_C_HDR_OK
  222. self.log('{} Different but no extern C - FAILED'.format(header), True)
  223. return self.PREPROC_OUT_DIFFERENT_NO_EXT_C_HDR_FAILED
  224. finally:
  225. os.unlink(cpp_out_file)
  226. try:
  227. os.unlink(c99_out_file)
  228. except Exception:
  229. pass
  230. # Get compilation data from an example to list all public header files
  231. def list_public_headers(self, ignore_dirs: List, ignore_files: Union[List, Set], only_dir: str=None) -> None:
  232. idf_path = os.getenv('IDF_PATH')
  233. if idf_path is None:
  234. raise RuntimeError("Environment variable 'IDF_PATH' wasn't set.")
  235. project_dir = os.path.join(idf_path, 'examples', 'get-started', 'blink')
  236. build_dir = tempfile.mkdtemp()
  237. sdkconfig = os.path.join(build_dir, 'sdkconfig')
  238. try:
  239. os.unlink(os.path.join(project_dir, 'sdkconfig'))
  240. except FileNotFoundError:
  241. pass
  242. subprocess.check_call(['idf.py', '-B', build_dir, f'-DSDKCONFIG={sdkconfig}', 'reconfigure'], cwd=project_dir)
  243. build_commands_json = os.path.join(build_dir, 'compile_commands.json')
  244. with open(build_commands_json, 'r', encoding='utf-8') as f:
  245. build_command = json.load(f)[0]['command'].split()
  246. include_dir_flags = []
  247. include_dirs = []
  248. # process compilation flags (includes and defines)
  249. for item in build_command:
  250. if item.startswith('-I'):
  251. include_dir_flags.append(item)
  252. if 'components' in item:
  253. include_dirs.append(item[2:]) # Removing the leading "-I"
  254. if item.startswith('-D'):
  255. include_dir_flags.append(item.replace('\\','')) # removes escaped quotes, eg: -DMBEDTLS_CONFIG_FILE=\\\"mbedtls/esp_config.h\\\"
  256. include_dir_flags.append('-I' + os.path.join(build_dir, 'config'))
  257. include_dir_flags.append('-DCI_HEADER_CHECK')
  258. sdkconfig_h = os.path.join(build_dir, 'config', 'sdkconfig.h')
  259. # prepares a main_c file for easier sdkconfig checks and avoid compilers warning when compiling headers directly
  260. with open(sdkconfig_h, 'a') as f:
  261. f.write('#define IDF_SDKCONFIG_INCLUDED')
  262. main_c = os.path.join(build_dir, 'compile.c')
  263. with open(main_c, 'w') as f:
  264. f.write('#if defined(IDF_CHECK_SDKCONFIG_INCLUDED) && ! defined(IDF_SDKCONFIG_INCLUDED)\n'
  265. '#error CONFIG_VARS_USED_WHILE_SDKCONFIG_NOT_INCLUDED\n'
  266. '#endif')
  267. # processes public include dirs, removing ignored files
  268. all_include_files = []
  269. files_to_check = []
  270. for d in include_dirs:
  271. if only_dir is not None and not os.path.relpath(d, idf_path).startswith(os.path.relpath(only_dir, idf_path)):
  272. self.log('{} - directory ignored (not in "{}")'.format(d, only_dir))
  273. continue
  274. if os.path.relpath(d, idf_path).startswith(tuple(ignore_dirs)):
  275. self.log('{} - directory ignored'.format(d))
  276. continue
  277. for root, dirnames, filenames in os.walk(d):
  278. for filename in fnmatch.filter(filenames, '*.h'):
  279. all_include_files.append(os.path.join(root, filename))
  280. self.main_c = main_c
  281. self.include_dir_flags = include_dir_flags
  282. ignore_files = set(ignore_files)
  283. # processes public include files, removing ignored files
  284. for file_name in all_include_files:
  285. rel_path_file = os.path.relpath(file_name, idf_path)
  286. if any([os.path.commonprefix([d, rel_path_file]) == d for d in ignore_dirs]):
  287. self.log('{} - file ignored (inside ignore dir)'.format(file_name))
  288. continue
  289. if rel_path_file in ignore_files:
  290. self.log('{} - file ignored'.format(file_name))
  291. continue
  292. files_to_check.append(file_name)
  293. # removes duplicates and places headers to a work queue
  294. for file_name in set(files_to_check):
  295. self.job_queue.put(file_name)
  296. self.job_queue.put(None) # to indicate the last job
  297. def check_all_headers() -> None:
  298. parser = argparse.ArgumentParser('Public header checker file', formatter_class=argparse.RawDescriptionHelpFormatter, epilog='''\
  299. Tips for fixing failures reported by this script
  300. ------------------------------------------------
  301. This checker validates all public headers to detect these types of issues:
  302. 1) "Sdkconfig Error": Using SDK config macros without including "sdkconfig.h"
  303. * Check if the failing include file or any other included file uses "CONFIG_..." prefixed macros
  304. 2) "Header Build Error": Header itself is not compilable (missing includes, macros, types)
  305. * Check that all referenced macros, types are available (defined or included)
  306. * Check that all included header files are available (included in paths)
  307. * Check for possible compilation issues
  308. * If only the C++ compilation fails, check that the header is C++ compatible
  309. * Try to compile only the offending header file
  310. 3) "Header Missing C++ Guard": Preprocessing the header by C and C++ should produce different output
  311. * Check if the "#ifdef __cplusplus" header sentinels are present
  312. 4) "Header Produced non-zero object": Header contains some object, a definition
  313. * Check if no definition is present in the offending header file
  314. 5) "Header contains _Static_assert or static_assert": Makes the use of _Static_assert or static_assert
  315. functions instead of using ESP_STATIC_ASSERT macro
  316. Notes:
  317. * The script validates *all* header files (recursively) in public folders for all components.
  318. * The script locates include paths from running a default build of "examples/get-started/blink'
  319. * The script does not support any other targets than esp32
  320. General tips:
  321. * Use "-d" argument to make the script check only the offending header file
  322. * Use "-v" argument to produce more verbose output
  323. * Copy, paste and execute the compilation commands to reproduce build errors (script prints out
  324. the entire compilation command line with absolute paths)
  325. ''')
  326. parser.add_argument('--verbose', '-v', help='enables verbose mode', action='store_true')
  327. parser.add_argument('--jobs', '-j', help='number of jobs to run checker', default=1, type=int)
  328. parser.add_argument('--prefix', '-p', help='compiler prefix', default='xtensa-esp32-elf-', type=str)
  329. parser.add_argument('--exclude-file', '-e', help='exception file', default='check_public_headers_exceptions.txt', type=str)
  330. parser.add_argument('--only-dir', '-d', help='reduce the analysis to this directory only', default=None, type=str)
  331. args = parser.parse_args()
  332. # process excluded files and dirs
  333. exclude_file = os.path.join(os.path.dirname(__file__), args.exclude_file)
  334. with open(exclude_file, 'r', encoding='utf-8') as f:
  335. lines = [line.rstrip() for line in f]
  336. ignore_files = []
  337. ignore_dirs = []
  338. for line in lines:
  339. if not line or line.isspace() or line.startswith('#'):
  340. continue
  341. if os.path.isdir(line):
  342. ignore_dirs.append(line)
  343. else:
  344. ignore_files.append(line)
  345. # start header check
  346. with PublicHeaderChecker(args.verbose, args.jobs, args.prefix) as header_check:
  347. header_check.list_public_headers(ignore_dirs, ignore_files, only_dir=args.only_dir)
  348. try:
  349. header_check.join()
  350. failures = header_check.get_failed()
  351. if len(failures) > 0:
  352. for failed in failures:
  353. print(failed)
  354. print(parser.epilog)
  355. exit(1)
  356. print('No errors found')
  357. except KeyboardInterrupt:
  358. print('Keyboard interrupt')
  359. if __name__ == '__main__':
  360. check_all_headers()