@@ -255,6 +255,130 @@ def _parse(self, text):
255255 return parse_end_pos
256256
257257
258+ class KbuildDtcError (Error ):
259+ """Models an error emitted by the Device Tree Compiler (DTC)."""
260+
261+ _source_diagnostic_re = re .compile (
262+ rf'^{ TIMESTAMP } (?P<kind>Lexical error|Error): '
263+ r'(?P<src_file>.*?):'
264+ r'(?P<location>\d+\.\d+(?:-\d+(?:\.\d+)?)?) '
265+ r'(?P<message>.*)$' ,
266+ flags = re .MULTILINE ,
267+ )
268+ _check_diagnostic_re = re .compile (
269+ rf'^{ TIMESTAMP } (?P<src_file>.*?)'
270+ r'(?::(?P<location>\d+\.\d+(?:-\d+(?:\.\d+)?)?))?'
271+ r': ERROR \((?P<check>[^)]+)\): (?P<message>.*)$' ,
272+ flags = re .MULTILINE ,
273+ )
274+ _fatal_re = re .compile (
275+ rf'^{ TIMESTAMP } FATAL ERROR: (?P<message>.*)$' ,
276+ flags = re .MULTILINE ,
277+ )
278+ _check_terminal_re = re .compile (
279+ rf'^{ TIMESTAMP } ERROR: Input tree has errors,.*$' ,
280+ flags = re .MULTILINE ,
281+ )
282+
283+ def __init__ (self , script = None , target = None ):
284+ super ().__init__ ()
285+ self .script = script
286+ self .target = target
287+ self .src_file = ""
288+ self .location = ""
289+ self .check = ""
290+
291+ @classmethod
292+ def has_diagnostic (cls , text ):
293+ """Return whether *text* contains a structured DTC error."""
294+ return any (regex .search (text ) for regex in (
295+ cls ._source_diagnostic_re ,
296+ cls ._check_diagnostic_re ,
297+ cls ._fatal_re ,
298+ ))
299+
300+ def _set_report_end (self , text , match , diagnostic_type ):
301+ """Find the terminal line belonging to a DTC diagnostic."""
302+ report_end = match .end ()
303+ following_text = text [report_end :]
304+
305+ if diagnostic_type == "source" :
306+ terminal_match = re .match (
307+ rf'\n{ TIMESTAMP } FATAL ERROR: .*' ,
308+ following_text ,
309+ )
310+ elif diagnostic_type == "check" :
311+ terminal_match = self ._check_terminal_re .search (
312+ text , match .end ()
313+ )
314+ else :
315+ terminal_match = None
316+
317+ if terminal_match :
318+ report_end = (
319+ report_end + terminal_match .end ()
320+ if diagnostic_type == "source"
321+ else terminal_match .end ()
322+ )
323+ return report_end
324+
325+ def _parse (self , text ):
326+ """Extract the last DTC diagnostic preceding the Make failure."""
327+ candidates = []
328+ for diagnostic_type , regex in (
329+ ("source" , self ._source_diagnostic_re ),
330+ ("check" , self ._check_diagnostic_re ),
331+ ):
332+ matches = list (regex .finditer (text ))
333+ if matches :
334+ candidates .append ((diagnostic_type , matches [- 1 ]))
335+
336+ if candidates :
337+ diagnostic_type , match = max (
338+ candidates , key = lambda candidate : candidate [1 ].start ()
339+ )
340+ else :
341+ fatal_matches = list (self ._fatal_re .finditer (text ))
342+ if not fatal_matches :
343+ return 0
344+ diagnostic_type = "fatal"
345+ match = fatal_matches [- 1 ]
346+
347+ if diagnostic_type == "source" :
348+ kind = match .group ('kind' )
349+ self .error_type = (
350+ "kbuild.dtc.lexical_error"
351+ if kind == "Lexical error"
352+ else "kbuild.dtc.error"
353+ )
354+ self .src_file = match .group ('src_file' )
355+ self .location = match .group ('location' )
356+ self .error_summary = f"{ kind } : { match .group ('message' )} "
357+ elif diagnostic_type == "check" :
358+ self .error_type = "kbuild.dtc.check_error"
359+ self .src_file = match .group ('src_file' )
360+ self .location = match .group ('location' ) or ""
361+ self .check = match .group ('check' )
362+ self .error_summary = match .group ('message' )
363+ else :
364+ self .error_type = "kbuild.dtc.fatal_error"
365+ self .error_summary = match .group ('message' )
366+ file_match = re .search (
367+ r'(?:Couldn.t open|Error closing) "(?P<src_file>[^"]+)"' ,
368+ self .error_summary ,
369+ )
370+ if file_match :
371+ self .src_file = file_match .group ('src_file' )
372+
373+ for field in ('src_file' , 'location' , 'check' ):
374+ if getattr (self , field ):
375+ self ._signature_fields .append (field )
376+
377+ report_end = self ._set_report_end (text , match , diagnostic_type )
378+ self ._report = text [match .start ():report_end ] + "\n "
379+ return report_end
380+
381+
258382class KbuildProcessError (Error ):
259383 """Models the information extracted from a kbuild error caused by a
260384 script, configuration or other runtime error.
@@ -458,6 +582,14 @@ def _is_kbuild_target(target):
458582 return False
459583
460584
585+ def _is_dtc_target (script , target ):
586+ """Return whether a Make failure belongs to a DTC build target."""
587+ return (
588+ target .endswith (('.dtb' , '.dtbo' ))
589+ or os .path .basename (script .split (':' , maxsplit = 1 )[0 ]) == 'Makefile.dtbs'
590+ )
591+
592+
461593def _find_script_target (error_str , text ):
462594 match = re .search (r'\[(?P<script>.*?): (?P<target>.*?)\] Error' , error_str )
463595 if not match :
@@ -510,7 +642,13 @@ def find_kbuild_error(text):
510642 logging .debug (f"[find_kbuild_error] script: { script } , target: { target } " )
511643 error = None
512644 # Kbuild error classification
513- if _is_object_file (target ) or _is_other_compiler_target (target , text [:start ]):
645+ error_text = text [:start ]
646+ if (
647+ _is_dtc_target (script , target )
648+ and KbuildDtcError .has_diagnostic (error_text )
649+ ):
650+ error = KbuildDtcError (script = script , target = target )
651+ elif _is_object_file (target ) or _is_other_compiler_target (target , error_text ):
514652 error = KbuildCompilerError (script = script , target = target )
515653 elif 'modpost' in script :
516654 error = KbuildModpostError (script = script , target = target )
@@ -519,8 +657,7 @@ def find_kbuild_error(text):
519657 else :
520658 # Catch-all condition for non-specific errors
521659 error = KbuildGenericError (script = script , target = target )
522- text = text [:start ]
523- error .parse (text )
660+ error .parse (error_text )
524661 else :
525662 # Unrecognized error, these are marked as unknown and not parsed
526663 error = KbuildUnknownError (error_str )
0 commit comments