2121_FILENAME_PATTERN = r'^\..*\.cmd$'
2222_LINE_PATTERN = r'^(saved)?cmd_[^ ]*\.o := (?P<command_prefix>.* )(?P<file_path>[^ ]*\.[cS]) *(;|$)'
2323_VALID_LOG_LEVELS = ['DEBUG' , 'INFO' , 'WARNING' , 'ERROR' , 'CRITICAL' ]
24+
25+ # Pre-compiled regexes for better performance
26+ _INCLUDE_PATTERN = re .compile (r'^\s*#\s*include\s*[<"]([^>"]*)[>"]' )
27+ _C_INCLUDE_PATTERN = re .compile (r'^\s*#\s*include\s*"([^"]*\.c)"\s*$' )
28+ _FILENAME_MATCHER = re .compile (_FILENAME_PATTERN )
29+
2430# The tools/ directory adopts a different build system, and produces .cmd
2531# files in a different format. Do not support it.
2632_EXCLUDE_DIRS = ['.git' , 'Documentation' , 'include' , 'tools' ]
@@ -82,7 +88,6 @@ def cmdfiles_in_dir(directory):
8288 The path to a .cmd file.
8389 """
8490
85- filename_matcher = re .compile (_FILENAME_PATTERN )
8691 exclude_dirs = [ os .path .join (directory , d ) for d in _EXCLUDE_DIRS ]
8792
8893 for dirpath , dirnames , filenames in os .walk (directory , topdown = True ):
@@ -92,7 +97,7 @@ def cmdfiles_in_dir(directory):
9297 continue
9398
9499 for filename in filenames :
95- if filename_matcher .match (filename ):
100+ if _FILENAME_MATCHER .match (filename ):
96101 yield os .path .join (dirpath , filename )
97102
98103
@@ -149,8 +154,87 @@ def cmdfiles_for_modorder(modorder):
149154 yield to_cmdfile (mod_line .rstrip ())
150155
151156
157+ def extract_includes_from_file (source_file , root_directory ):
158+ """Extract #include statements from a C file.
159+
160+ Args:
161+ source_file: Path to the source .c file to analyze
162+ root_directory: Root directory for resolving relative paths
163+
164+ Returns:
165+ List of header files that should be included (without quotes/brackets)
166+ """
167+ includes = []
168+ if not os .path .exists (source_file ):
169+ return includes
170+
171+ try :
172+ with open (source_file , 'r' ) as f :
173+ for line in f :
174+ line = line .strip ()
175+ # Look for #include statements.
176+ # Match both #include "header.h" and #include <header.h>.
177+ match = _INCLUDE_PATTERN .match (line )
178+ if match :
179+ header = match .group (1 )
180+ # Skip including other .c files to avoid circular includes.
181+ if not header .endswith ('.c' ):
182+ # For relative includes (quoted), resolve path relative to source file.
183+ if '"' in line :
184+ src_dir = os .path .dirname (source_file )
185+ header_path = os .path .join (src_dir , header )
186+ if os .path .exists (header_path ):
187+ rel_header = os .path .relpath (header_path , root_directory )
188+ includes .append (rel_header )
189+ else :
190+ includes .append (header )
191+ else :
192+ # System include like <linux/sched.h>.
193+ includes .append (header )
194+ except IOError :
195+ pass
196+
197+ return includes
198+
199+
200+ def find_included_c_files (source_file , root_directory ):
201+ """Find .c files that are included by the given source file.
202+
203+ Args:
204+ source_file: Path to the source .c file
205+ root_directory: Root directory for resolving relative paths
206+
207+ Yields:
208+ Full paths to included .c files
209+ """
210+ if not os .path .exists (source_file ):
211+ return
212+
213+ try :
214+ with open (source_file , 'r' ) as f :
215+ for line in f :
216+ line = line .strip ()
217+ # Look for #include "*.c" patterns.
218+ match = _C_INCLUDE_PATTERN .match (line )
219+ if match :
220+ included_file = match .group (1 )
221+ # Handle relative paths.
222+ if not os .path .isabs (included_file ):
223+ src_dir = os .path .dirname (source_file )
224+ included_file = os .path .join (src_dir , included_file )
225+
226+ # Normalize the path.
227+ included_file = os .path .normpath (included_file )
228+
229+ # Check if the file exists.
230+ if os .path .exists (included_file ):
231+ yield included_file
232+ except IOError :
233+ pass
234+
235+
152236def process_line (root_directory , command_prefix , file_path ):
153- """Extracts information from a .cmd line and creates an entry from it.
237+ """Extracts information from a .cmd line and creates entries from it.
154238
155239 Args:
156240 root_directory: The directory that was searched for .cmd files. Usually
@@ -160,7 +244,8 @@ def process_line(root_directory, command_prefix, file_path):
160244 Usually relative to root_directory, but sometimes absolute.
161245
162246 Returns:
163- An entry to append to compile_commands.
247+ A list of entries to append to compile_commands (may include multiple
248+ entries if the source file includes other .c files).
164249
165250 Raises:
166251 ValueError: Could not find the extracted file based on file_path and
@@ -176,11 +261,47 @@ def process_line(root_directory, command_prefix, file_path):
176261 abs_path = os .path .realpath (os .path .join (root_directory , file_path ))
177262 if not os .path .exists (abs_path ):
178263 raise ValueError ('File %s not found' % abs_path )
179- return {
264+
265+ entries = []
266+
267+ # Create entry for the main source file.
268+ main_entry = {
180269 'directory' : root_directory ,
181270 'file' : abs_path ,
182271 'command' : prefix + file_path ,
183272 }
273+ entries .append (main_entry )
274+
275+ # Find and create entries for included .c files.
276+ for included_c_file in find_included_c_files (abs_path , root_directory ):
277+ # For included .c files, create a compilation command that:
278+ # 1. Uses the same compilation flags as the parent file
279+ # 2. But compiles the included file directly (not the parent)
280+ # 3. Includes necessary headers from the parent file for proper macro resolution
281+
282+ # Convert absolute path to relative for the command.
283+ rel_path = os .path .relpath (included_c_file , root_directory )
284+
285+ # Extract includes from the parent file to provide proper compilation context.
286+ extra_includes = ''
287+ try :
288+ parent_includes = extract_includes_from_file (abs_path , root_directory )
289+ if parent_includes :
290+ extra_includes = ' ' + ' ' .join ('-include ' + inc for inc in parent_includes )
291+ except IOError :
292+ pass
293+
294+ included_entry = {
295+ 'directory' : root_directory ,
296+ 'file' : included_c_file ,
297+ # Use the same compilation prefix but target the included file directly.
298+ # Add extra headers for proper macro resolution.
299+ 'command' : prefix + extra_includes + ' ' + rel_path ,
300+ }
301+ entries .append (included_entry )
302+ logging .debug ('Added entry for included file: %s' , included_c_file )
303+
304+ return entries
184305
185306
186307def main ():
@@ -213,9 +334,9 @@ def main():
213334 result = line_matcher .match (f .readline ())
214335 if result :
215336 try :
216- entry = process_line (directory , result .group ('command_prefix' ),
337+ entries = process_line (directory , result .group ('command_prefix' ),
217338 result .group ('file_path' ))
218- compile_commands .append ( entry )
339+ compile_commands .extend ( entries )
219340 except ValueError as err :
220341 logging .info ('Could not add line from %s: %s' ,
221342 cmdfile , err )
0 commit comments