22#
33# Checks all public headers in IDF in the ci
44#
5- # SPDX-FileCopyrightText: 2020-2024 Espressif Systems (Shanghai) CO LTD
5+ # SPDX-FileCopyrightText: 2020-2025 Espressif Systems (Shanghai) CO LTD
66# SPDX-License-Identifier: Apache-2.0
77#
88import argparse
2424
2525class HeaderFailed (Exception ):
2626 """Base header failure exception"""
27+
2728 pass
2829
2930
@@ -62,15 +63,17 @@ def __str__(self) -> str:
6263
6364# Creates a temp file and returns both output as a string and a file name
6465#
65- def exec_cmd_to_temp_file (what : List , suffix : str = '' ) -> Tuple [int , str , str , str , str ]:
66+ def exec_cmd_to_temp_file (what : List , suffix : str = '' ) -> Tuple [int , str , str , str , str ]:
6667 out_file = tempfile .NamedTemporaryFile (suffix = suffix , delete = False )
6768 rc , out , err , cmd = exec_cmd (what , out_file )
6869 with open (out_file .name , 'r' , encoding = 'utf-8' ) as f :
6970 out = f .read ()
7071 return rc , out , err , out_file .name , cmd
7172
7273
73- def exec_cmd (what : List , out_file : Union ['tempfile._TemporaryFileWrapper[bytes]' , int ]= subprocess .PIPE ) -> Tuple [int , str , str , str ]:
74+ def exec_cmd (
75+ what : List , out_file : Union ['tempfile._TemporaryFileWrapper[bytes]' , int ] = subprocess .PIPE
76+ ) -> Tuple [int , str , str , str ]:
7477 p = subprocess .Popen (what , stdin = subprocess .PIPE , stdout = out_file , stderr = subprocess .PIPE )
7578 output_b , err_b = p .communicate ()
7679 rc = p .returncode
@@ -80,12 +83,11 @@ def exec_cmd(what: List, out_file: Union['tempfile._TemporaryFileWrapper[bytes]'
8083
8184
8285class PublicHeaderChecker :
83-
84- def log (self , message : str , debug : bool = False ) -> None :
86+ def log (self , message : str , debug : bool = False ) -> None :
8587 if self .verbose or debug :
8688 print (message )
8789
88- def __init__ (self , verbose : bool = False , jobs : int = 1 , prefix : Optional [str ]= None ) -> None :
90+ def __init__ (self , verbose : bool = False , jobs : int = 1 , prefix : Optional [str ] = None ) -> None :
8991 self .gcc = '{}gcc' .format (prefix )
9092 self .gpp = '{}g++' .format (prefix )
9193 self .verbose = verbose
@@ -97,7 +99,9 @@ def __init__(self, verbose: bool=False, jobs: int=1, prefix: Optional[str]=None)
9799 self .kconfig_macro = re .compile (r'\bCONFIG_[A-Z0-9_]+' )
98100 self .static_assert = re .compile (r'(_Static_assert|static_assert)' )
99101 self .defines_assert = re .compile (r'#define[ \t]+ESP_STATIC_ASSERT' )
100- self .auto_soc_header = re .compile (r'components/soc/esp[a-z0-9_]+(?:/\w+)?/(include|register)/(soc|modem)/[a-zA-Z0-9_]+.h' )
102+ self .auto_soc_header = re .compile (
103+ r'components/soc/esp[a-z0-9_]+(?:/\w+)?/(include|register)/(soc|modem|hw_ver\d+/soc)/[a-zA-Z0-9_]+.h'
104+ )
101105 self .assembly_nocode = r'^\s*(\.file|\.text|\.ident|\.option|\.attribute|(\.section)?).*$'
102106 self .check_threads : List [Thread ] = []
103107 self .stdc = '--std=c99'
@@ -109,7 +113,7 @@ def __init__(self, verbose: bool=False, jobs: int=1, prefix: Optional[str]=None)
109113
110114 def __enter__ (self ) -> 'PublicHeaderChecker' :
111115 for i in range (self .jobs ):
112- t = Thread (target = self .check_headers , args = (i , ))
116+ t = Thread (target = self .check_headers , args = (i ,))
113117 self .check_threads .append (t )
114118 t .start ()
115119 return self
@@ -155,7 +159,9 @@ def check_one_header(self, header: str, num: int) -> None:
155159
156160 # Checks if the header contains some assembly code and whether it is compilable
157161 def compile_one_header_with (self , compiler : str , std_flags : str , header : str ) -> None :
158- rc , out , err , cmd = exec_cmd ([compiler , std_flags , '-S' , '-o-' , '-include' , header , self .main_c ] + self .include_dir_flags )
162+ rc , out , err , cmd = exec_cmd (
163+ [compiler , std_flags , '-S' , '-o-' , '-include' , header , self .main_c ] + self .include_dir_flags
164+ )
159165 if rc == 0 :
160166 if not re .sub (self .assembly_nocode , '' , out , flags = re .M ).isspace ():
161167 raise HeaderFailedContainsCode ()
@@ -185,16 +191,25 @@ def compile_one_header_with(self, compiler: str, std_flags: str, header: str) ->
185191 # - We still have some code? -> FAIL the test (our header needs extern "C")
186192 # - Only whitespaces -> header is OK (it contains only macros and directives)
187193 def preprocess_one_header (self , header : str , num : int ) -> None :
188- all_compilation_flags = ['-w' , '-P' , '-E' , '-DESP_PLATFORM' , '-include' , header , self .main_c ] + self .include_dir_flags
194+ all_compilation_flags = [
195+ '-w' ,
196+ '-P' ,
197+ '-E' ,
198+ '-DESP_PLATFORM' ,
199+ '-include' ,
200+ header ,
201+ self .main_c ,
202+ ] + self .include_dir_flags
189203 # just strip comments to check for CONFIG_... macros or static asserts
190- rc , out , err , _ = exec_cmd ([self .gcc , '-fpreprocessed' , '-dD' , '-P' , '-E' , header ] + self .include_dir_flags )
191- # we ignore the rc here, as the `-fpreprocessed` flag expects the file to have macros already expanded, so we might get some errors
192- # here we use it only to remove comments (even if the command returns non-zero code it produces the correct output)
204+ rc , out , err , _ = exec_cmd ([self .gcc , '-fpreprocessed' , '-dD' , '-P' , '-E' , header ] + self .include_dir_flags )
205+ # we ignore the rc here, as the `-fpreprocessed` flag expects the file to have macros already expanded,
206+ # so we might get some errors here we use it only to remove comments (even if the command returns non-zero
207+ # code it produces the correct output)
193208 if re .search (self .kconfig_macro , out ):
194209 # enable defined #error if sdkconfig.h not included
195210 all_compilation_flags .append ('-DIDF_CHECK_SDKCONFIG_INCLUDED' )
196- # If the file contain _Static_assert or static_assert, make sure it doesn't not define ESP_STATIC_ASSERT and that it
197- # is not an automatically generated soc header file
211+ # If the file contain _Static_assert or static_assert, make sure it doesn't not define ESP_STATIC_ASSERT
212+ # and that it is not an automatically generated soc header file
198213 grp = re .search (self .static_assert , out )
199214 # Normalize the potential A//B, A/./B, A/../A, from the name
200215 normalized_path = os .path .normpath (header )
@@ -234,19 +249,22 @@ def preprocess_one_header(self, header: str, num: int) -> None:
234249 if re .search (self .extern_c , out ):
235250 self .log ('{} extern C present in the actual header, too - OK' .format (header ))
236251 return
237- # at this point we know that the header itself is missing extern-C, so we need to check if it contains an actual *code*
238- # we remove all preprocessor's directive to check if there's any code besides macros
252+ # at this point we know that the header itself is missing extern-C, so we need to check if it
253+ # contains an actual *code* we remove all preprocessor's directive to check if there's any code
254+ # besides macros
239255 macros = re .compile (r'(?m)^\s*#(?:.*\\\r?\n)*.*$' ) # Matches multiline preprocessor directives
240256 without_macros = macros .sub ('' , out )
241257 if without_macros .isspace ():
242258 self .log ("{} Header doesn't need extern-C, it's all just macros - OK" .format (header ))
243259 return
244- # at this point we know that the header is not only composed of macro definitions, but could just contain some "harmless" macro calls
245- # let's remove them and check again
246- macros_calls = r'(.*?)ESP_STATIC_ASSERT[^;]+;' # static assert macro only, we could add more if needed
260+ # at this point we know that the header is not only composed of macro definitions, but could
261+ # just contain some "harmless" macro calls let's remove them and check again
262+ macros_calls = r'(.*?)ESP_STATIC_ASSERT[^;]+;' # static assert macro only, we could add more if needed
247263 without_macros = re .sub (macros_calls , '' , without_macros , flags = re .DOTALL )
248264 if without_macros .isspace ():
249- self .log ("{} Header doesn't need extern-C, it's all macros definitions and calls - OK" .format (header ))
265+ self .log (
266+ "{} Header doesn't need extern-C, it's all macros definitions and calls - OK" .format (header )
267+ )
250268 return
251269
252270 self .log ('{} Different but no extern C - FAILED' .format (header ), True )
@@ -259,7 +277,9 @@ def preprocess_one_header(self, header: str, num: int) -> None:
259277 pass
260278
261279 # Get compilation data from an example to list all public header files
262- def list_public_headers (self , ignore_dirs : List , ignore_files : Union [List , Set ], only_dir : Optional [str ]= None ) -> None :
280+ def list_public_headers (
281+ self , ignore_dirs : List , ignore_files : Union [List , Set ], only_dir : Optional [str ] = None
282+ ) -> None :
263283 idf_path = os .getenv ('IDF_PATH' )
264284 if idf_path is None :
265285 raise RuntimeError ("Environment variable 'IDF_PATH' wasn't set." )
@@ -275,7 +295,7 @@ def list_public_headers(self, ignore_dirs: List, ignore_files: Union[List, Set],
275295 def get_std (json : List , extension : str ) -> str :
276296 # compile commands for the files with specified extension, containing C(XX) standard flag
277297 command = [c for c in j if c ['file' ].endswith ('.' + extension ) and '-std=' in c ['command' ]][0 ]
278- return str ([s for s in command ['command' ].split () if 'std=' in s ][0 ]) # grab the std flag
298+ return str ([s for s in command ['command' ].split () if 'std=' in s ][0 ]) # grab the std flag
279299
280300 build_commands_json = os .path .join (build_dir , 'compile_commands.json' )
281301 with open (build_commands_json , 'r' , encoding = 'utf-8' ) as f :
@@ -292,7 +312,9 @@ def get_std(json: List, extension: str) -> str:
292312 if 'components' in item :
293313 include_dirs .append (item [2 :]) # Removing the leading "-I"
294314 if item .startswith ('-D' ):
295- include_dir_flags .append (item .replace ('\\ ' ,'' )) # removes escaped quotes, eg: -DMBEDTLS_CONFIG_FILE=\\\"mbedtls/esp_config.h\\\"
315+ include_dir_flags .append (
316+ item .replace ('\\ ' , '' )
317+ ) # removes escaped quotes, eg: -DMBEDTLS_CONFIG_FILE=\\\"mbedtls/esp_config.h\\\"
296318 include_dir_flags .append ('-I' + os .path .join (build_dir , 'config' ))
297319 include_dir_flags .append ('-DCI_HEADER_CHECK' )
298320 sdkconfig_h = os .path .join (build_dir , 'config' , 'sdkconfig.h' )
@@ -301,14 +323,18 @@ def get_std(json: List, extension: str) -> str:
301323 f .write ('#define IDF_SDKCONFIG_INCLUDED' )
302324 main_c = os .path .join (build_dir , 'compile.c' )
303325 with open (main_c , 'w' ) as f :
304- f .write ('#if defined(IDF_CHECK_SDKCONFIG_INCLUDED) && ! defined(IDF_SDKCONFIG_INCLUDED)\n '
305- '#error CONFIG_VARS_USED_WHILE_SDKCONFIG_NOT_INCLUDED\n '
306- '#endif' )
326+ f .write (
327+ '#if defined(IDF_CHECK_SDKCONFIG_INCLUDED) && ! defined(IDF_SDKCONFIG_INCLUDED)\n '
328+ '#error CONFIG_VARS_USED_WHILE_SDKCONFIG_NOT_INCLUDED\n '
329+ '#endif'
330+ )
307331 # processes public include dirs, removing ignored files
308332 all_include_files = []
309333 files_to_check = []
310334 for d in include_dirs :
311- if only_dir is not None and not os .path .relpath (d , idf_path ).startswith (os .path .relpath (only_dir , idf_path )):
335+ if only_dir is not None and not os .path .relpath (d , idf_path ).startswith (
336+ os .path .relpath (only_dir , idf_path )
337+ ):
312338 self .log ('{} - directory ignored (not in "{}")' .format (d , only_dir ))
313339 continue
314340 if os .path .relpath (d , idf_path ).startswith (tuple (ignore_dirs )):
@@ -337,7 +363,10 @@ def get_std(json: List, extension: str) -> str:
337363
338364
339365def check_all_headers () -> None :
340- parser = argparse .ArgumentParser ('Public header checker file' , formatter_class = argparse .RawDescriptionHelpFormatter , epilog = '''\
366+ parser = argparse .ArgumentParser (
367+ 'Public header checker file' ,
368+ formatter_class = argparse .RawDescriptionHelpFormatter ,
369+ epilog = """\
341370 Tips for fixing failures reported by this script
342371 ------------------------------------------------
343372 This checker validates all public headers to detect these types of issues:
@@ -366,11 +395,14 @@ def check_all_headers() -> None:
366395 * Use "-v" argument to produce more verbose output
367396 * Copy, paste and execute the compilation commands to reproduce build errors (script prints out
368397 the entire compilation command line with absolute paths)
369- ''' )
398+ """ ,
399+ )
370400 parser .add_argument ('--verbose' , '-v' , help = 'enables verbose mode' , action = 'store_true' )
371401 parser .add_argument ('--jobs' , '-j' , help = 'number of jobs to run checker' , default = 1 , type = int )
372402 parser .add_argument ('--prefix' , '-p' , help = 'compiler prefix' , default = 'xtensa-esp32-elf-' , type = str )
373- parser .add_argument ('--exclude-file' , '-e' , help = 'exception file' , default = 'check_public_headers_exceptions.txt' , type = str )
403+ parser .add_argument (
404+ '--exclude-file' , '-e' , help = 'exception file' , default = 'check_public_headers_exceptions.txt' , type = str
405+ )
374406 parser .add_argument ('--only-dir' , '-d' , help = 'reduce the analysis to this directory only' , default = None , type = str )
375407 args = parser .parse_args ()
376408
0 commit comments