88==================== Timing Summaries (end) ====================
99We also stop at any other delimiter line made of ---- / ==== that is not itself
1010a Hierarchical utilization header.
11+
12+ If the section is missing, we invoke env-scripts make targets:
13+ make get_impl_log CPU=<cpu>
14+ (fallback) make get_synth_log CPU=<cpu>
15+ and always exit with code 3 so CI marks the step as failed for visibility.
1116"""
1217
1318import argparse
1419import os
1520import re
1621import sys
22+ import subprocess
1723
1824# Header patterns that indicate the beginning of the hierarchical utilization block.
1925HIER_HEADER_PATTERNS = [
@@ -69,13 +75,98 @@ def extract_section(text):
6975 collected .pop ()
7076 return '\n ' .join (collected ) + '\n '
7177
78+ def infer_cpu_from_path (path : str ) -> str :
79+ p = path .lower ()
80+ if 'xiangshan' in p :
81+ return 'xiangshan'
82+ return 'nutshell'
83+
84+ def try_print_logs_via_make (start_dir : str , cpu : str ) -> bool :
85+ """Invoke env-scripts make targets to print Vivado logs to stderr.
86+
87+ Tries get_impl_log first, then get_synth_log. Returns True if any output is printed.
88+ """
89+ # Run make inside env-scripts/fpga_diff (the vivado-analyse.txt directory)
90+ env_root = os .path .abspath (start_dir )
91+ sys .stderr .write (f"[ci.py] using env-scripts/fpga_diff at { env_root } \n " )
92+ printed_any = False
93+ for target in ('get_impl_log' , 'get_synth_log' ):
94+ try :
95+ sys .stderr .write (f"[ci.py] invoking make -C { env_root } { target } CPU={ cpu } \n " )
96+ proc = subprocess .run (
97+ ['make' , '-C' , env_root , target , f'CPU={ cpu } ' ],
98+ stdout = subprocess .PIPE ,
99+ stderr = subprocess .STDOUT ,
100+ text = True ,
101+ encoding = 'utf-8' ,
102+ errors = 'ignore' ,
103+ timeout = 600 ,
104+ )
105+ out = proc .stdout or ''
106+ if proc .returncode == 0 and out .strip ():
107+ sys .stderr .write (f"[ci.py] ---- BEGIN { target } (CPU={ cpu } ) ----\n " )
108+ sys .stderr .write (out )
109+ if not out .endswith ('\n ' ):
110+ sys .stderr .write ('\n ' )
111+ sys .stderr .write (f"[ci.py] ---- END { target } ----\n " )
112+ printed_any = True
113+ # If impl log printed, we can stop; otherwise try synth as fallback
114+ if target == 'get_impl_log' :
115+ return True
116+ else :
117+ sys .stderr .write (f"[ci.py] { target } failed (rc={ proc .returncode } ) or produced no usable output\n " )
118+ except Exception as e :
119+ sys .stderr .write (f"[ci.py] failed to run { target } : { e } \n " )
120+ return printed_any
121+
122+ # (legacy helper removed; log collection now delegated to make targets)
123+
124+ def try_print_runme_logs_by_path (start_dir : str , cpu : str ) -> bool :
125+ """Fallback: read known runme.log paths directly and print to stderr.
126+
127+ For nutshell:
128+ <start_dir>/fpga_nutshell/fpga_nutshell.runs/{impl_1,synth_1}/runme.log
129+ For xiangshan:
130+ <start_dir>/fpga_xiangshan/fpga_xiangshan.runs/{impl_1,synth_1}/runme.log
131+ """
132+ cpu = (cpu or 'nutshell' ).lower ()
133+ if 'xiangshan' in cpu :
134+ proj = os .path .join (start_dir , 'fpga_xiangshan' )
135+ runs_base = os .path .join (proj , 'fpga_xiangshan.runs' )
136+ else :
137+ proj = os .path .join (start_dir , 'fpga_nutshell' )
138+ runs_base = os .path .join (proj , 'fpga_nutshell.runs' )
139+
140+ printed = False
141+ for stage in ('impl_1' , 'synth_1' ):
142+ log_path = os .path .join (runs_base , stage , 'runme.log' )
143+ if os .path .exists (log_path ):
144+ try :
145+ with open (log_path , 'r' , encoding = 'utf-8' , errors = 'ignore' ) as lf :
146+ content = lf .read ()
147+ sys .stderr .write (f"[ci.py] ---- BEGIN { cpu } :{ stage } runme.log ----\n " )
148+ sys .stderr .write (content )
149+ if not content .endswith ('\n ' ):
150+ sys .stderr .write ('\n ' )
151+ sys .stderr .write (f"[ci.py] ---- END { cpu } :{ stage } runme.log ----\n " )
152+ printed = True
153+ if stage == 'impl_1' :
154+ # Prefer impl_1; if present, no need to fallback to synth_1
155+ return True
156+ except Exception as e :
157+ sys .stderr .write (f"[ci.py] failed reading { log_path } : { e } \n " )
158+ else :
159+ sys .stderr .write (f"[ci.py] log not found: { log_path } \n " )
160+ return printed
161+
72162def main ():
73163 ap = argparse .ArgumentParser (description = 'Extract "Hierarchical utilization" section from Vivado report.' )
74164 ap .add_argument ('-i' , '--input' , required = True , help = 'Path to vivado-analyse.txt' )
75165 ap .add_argument ('-o' , '--output' , help = 'Optional output file path' )
76166 ap .add_argument ('--format' , choices = ['text' , 'markdown' ], default = 'text' , help = 'Output formatting' )
77167 ap .add_argument ('--fail-missing' , action = 'store_true' ,
78168 help = 'Exit with code 3 if section missing (default: do not fail).' )
169+ ap .add_argument ('--cpu' , help = 'CPU name for env-scripts log helpers (e.g., nutshell, xiangshan). Optional.' )
79170 args = ap .parse_args ()
80171
81172 if not os .path .exists (args .input ):
@@ -87,12 +178,13 @@ def main():
87178
88179 section = extract_section (raw )
89180 if section is None :
90- msg = 'Hierarchical utilization section not found.'
91- if args .fail_missing :
92- sys .stderr .write ('[ci.py] ' + msg + '\n ' )
93- sys .exit (3 )
94- # Graceful: print note and exit 0
95- out_text = msg + '\n '
181+ base = os .path .dirname (os .path .abspath (args .input ))
182+ cpu = args .cpu or infer_cpu_from_path (base )
183+ ok = try_print_logs_via_make (base , cpu )
184+ if not ok :
185+ try_print_runme_logs_by_path (base , cpu )
186+ sys .stderr .write ('[ci.py] Hierarchical utilization section not found.\n ' )
187+ sys .exit (3 )
96188 else :
97189 if args .format == 'markdown' :
98190 out_text = 'Vivado Hierarchical utilization:\n \n ```txt\n ' + section .rstrip ('\n ' ) + '\n ```\n '
0 commit comments