Hog Hog2026.2-1
create_badges.tcl
Go to the documentation of this file.
1 #!/usr/bin/env tclsh
2 # Copyright 2018-2026 The University of Birmingham
3 # Copyright 2018-2026 Max-Planck-Institute for Physics
4 #
5 # Licensed under the Apache License, Version 2.0 (the "License");
6 # you may not use this file except in compliance with the License.
7 # You may obtain a copy of the License at
8 #
9 # http://www.apache.org/licenses/LICENSE-2.0
10 #
11 # Unless required by applicable law or agreed to in writing, software
12 # distributed under the License is distributed on an "AS IS" BASIS,
13 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 # See the License for the specific language governing permissions and
15 # limitations under the License.
16 
17 # @file
18 # Create and uploads GitLab badges for chosen projects
19 
20 
21 proc generate_prj_badge {prj_name ver color file {is_hls 0}} {
22  # Left-panel font auto-shrink (existing behaviour)
23  set font_size 11.0
24  set max_characters 20.0
25  if { [expr {[string length $prj_name] > $max_characters}] } {
26  set scaling_factor [expr {$max_characters / [string length $prj_name]}]
27  set font_size [expr {ceil($scaling_factor * $font_size)}]
28  }
29 
30  # Right-panel text: for HLS badges prepend "HLS " as a subtle marker
31  if {$is_hls} {
32  set ver_text "HLS $ver"
33  } else {
34  set ver_text $ver
35  }
36 
37  # Right-panel background colour. HLS badges get an indigo panel so they
38  # stand out from Vivado badges
39  if {$is_hls} {
40  set right_color "#4527A0"
41  } else {
42  set right_color "#262626"
43  }
44 
45  # Right-panel font auto-shrink, proportional to the narrower 90-px panel
46  set ver_font_size 11.0
47  set ver_max_characters 12.0
48  if { [expr {[string length $ver_text] > $ver_max_characters}] } {
49  set ver_scaling [expr {$ver_max_characters / [string length $ver_text]}]
50  set ver_font_size [expr {ceil($ver_scaling * $ver_font_size)}]
51  }
52 
53  set svg_content "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
54 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"250\" height=\"20\">
55  <linearGradient id=\"b\" x2=\"0\" y2=\"100%\">
56  <stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>
57  <stop offset=\"1\" stop-opacity=\".1\"/>
58  </linearGradient>
59  <mask id=\"hog_prj_badge\">
60  <rect width=\"250\" height=\"20\" rx=\"10\" fill=\"#fff\"/>
61  </mask>
62  <g mask=\"url(#hog_prj_badge)\">
63  <path fill=\"$color\" d=\"M0 0h250v20H0z\"/>
64  <path fill=\"$right_color\" d=\"M160 0h90v20H160z\"/>
65  <path fill=\"$right_color\" d=\"M250,20 a1,1 0 0,0 0,-16\"/>
66  <path fill=\"url(#b)\" d=\"M0 0h250v20H0z\"/>
67  </g>
68  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"$font_size\">
69  <text x=\"80\" y=\"14\">$prj_name</text>
70  </g>
71  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"$ver_font_size\">
72  <text x=\"205\" y=\"14\">$ver_text</text>
73  </g>
74 </svg>"
75 
76  if {
77  [catch {
78  set fh [open $file w]
79  puts $fh $svg_content
80  close $fh
81  } error_msg]
82  } {
83  error "Failed to write to file: $error_msg"
84  }
85 }
86 
87 proc generate_res_badge {res res_value color file {is_hls 0}} {
88  # Resource badges show only the percentage on the right panel — no "HLS"
89  # prefix here. The HLS marker is already conveyed by the timing/project
90  # badge (generate_prj_badge) for the same component, and repeating it on
91  # every resource badge eats the limited 60-px panel width
92  set value_text $res_value
93 
94  # Right-panel font auto-shrink (60-px panel)
95  set value_font_size 11.0
96  set value_max_characters 9.0
97  if { [expr {[string length $value_text] > $value_max_characters}] } {
98  set value_scaling [expr {$value_max_characters / [string length $value_text]}]
99  set value_font_size [expr {ceil($value_scaling * $value_font_size)}]
100  }
101 
102  set svg_content "<?xml version=\"1.0\" encoding=\"UTF-8\"?>
103 <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"120\" height=\"20\">
104  <linearGradient id=\"b\" x2=\"0\" y2=\"100%\">
105  <stop offset=\"0\" stop-color=\"#bbb\" stop-opacity=\".1\"/>
106  <stop offset=\"1\" stop-opacity=\".1\"/>
107  </linearGradient>
108  <mask id=\"hog_res_badge\">
109  <rect width=\"120\" height=\"20\" rx=\"3\" fill=\"#fff\"/>
110  </mask>
111  <g mask=\"url(#hog_res_badge)\">
112  <path fill=\"#555\" d=\"M0 0h60v20H0z\"/>
113  <path fill=\"$color\" d=\"M60 0h60v20H60z\"/>
114  <path fill=\"url(#b)\" d=\"M0 0h120v20H0z\"/>
115  </g>
116  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"11\">
117  <text x=\"30\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">$res</text>
118  <text x=\"30\" y=\"14\">$res</text>
119  </g>
120  <g fill=\"#fff\" text-anchor=\"middle\" font-family=\"DejaVu Sans,Verdana,Geneva,sans-serif\" font-size=\"$value_font_size\">
121  <text x=\"90\" y=\"15\" fill=\"#010101\" fill-opacity=\".3\">$value_text</text>
122  <text x=\"90\" y=\"14\">$value_text</text>
123  </g>
124 </svg>"
125 
126  if {
127  [catch {
128  set fh [open $file w]
129  puts $fh $svg_content
130  close $fh
131  } error_msg]
132  } {
133  error "Failed to write to file: $error_msg"
134  }
135 }
136 
137 set OldPath [pwd]
138 set TclPath [file dirname [info script]]/..
139 set repo_path [file normalize "$TclPath/../.."]
140 source $TclPath/hog.tcl
141 set curl_cmd [GetCurl]
142 
143 set usage "- CI script that creates GitLab badges with utilisation and timing results for a chosen Hog project.\n\
144 USAGE: $::argv0 <push token> <Gitlab api url> <Gitlab project id> <Gitlab project url> <GitLab Server URL> <Hog project|hls:component> <ext_path>\n\
145 \n\
146  <Hog project> Name of a Vivado project in Top/ — produces Vivado badges from bin/<project>-<ver>/utilization.txt\n\
147  hls:<component> Produces HLS badges for a single HLS component located at bin/*/\[vitis_hls/\]<component>/utilization.txt"
148 
149 if {[llength $argv] < 7} {
150  Msg Info [cmdline::usage $usage]
151  cd $OldPath
152  return
153 }
154 
155 set result [catch {package require json} JsonFound]
156 if {"$result" != "0"} {
157  Msg CriticalWarning "Cannot find JSON package equal or higher than 1.0.\n $JsonFound\n Exiting"
158  return -1
159 }
160 
161 set push_token [lindex $argv 0]
162 set api_url [lindex $argv 1]
163 set project_id [lindex $argv 2]
164 set project_url [lindex $argv 3]
165 set gitlab_url [lindex $argv 4]
166 set project [lindex $argv 5]
167 set ext_path [lindex $argv 6]
168 
169 set resources [dict create "LUTs" "LUTs" "Registers" "FFs" "Block" "BRAM" "URAM" "URAM" "DSPs" "DSPs"]
170 
171 # An entry of the form "hls:<component>" in HOG_BADGE_PROJECTS requests badges
172 # for a single HLS component rather than a Vivado project. HLS badge generation
173 # is strictly opt-in: components are only badged when the user lists them with
174 # the "hls:" prefix
175 set is_hls_badge 0
176 set hls_component ""
177 if {[string match "hls:*" $project]} {
178  set is_hls_badge 1
179  set hls_component [string range $project 4 end]
180 }
181 
182 if {!$is_hls_badge} {
183  set ver [GetProjectVersion $repo_path/Top/$project $repo_path $ext_path 0]
184 }
185 
186 set accumulated ""
187 set current_badges [dict create]
188 set page 0
189 
190 Msg Info "Retrieving current badges..."
191 while {1} {
192  lassign [ExecuteRet {*}$curl_cmd --header "PRIVATE-TOKEN: $push_token" "$api_url/projects/${project_id}/badges?page=$page" --request GET] ret content
193  set content_dict [json::json2dict $content]
194  if {[llength $content_dict] > 0} {
195  foreach it $content_dict {
196  dict set current_badges [DictGet $it name] [DictGet $it id]
197  }
198  incr page
199  } else {
200  break
201  }
202 }
203 
204 
205 # Extract a "vX.Y.Z" version string from a bin-directory name of the form
206 # "<project>-vX.Y.Z" or "<project>-vX.Y.Z-<sha>". Returns "" on no match
207 proc extract_ver_from_dir {dir_name} {
208  # The "--" separator stops regexp from interpreting the leading "-" of the
209  # pattern as an option flag.
210  if {[regexp -- {-(v[0-9]+\.[0-9]+\.[0-9]+)(?:-[0-9a-fA-F]+)?$} $dir_name -> v]} {
211  return $v
212  }
213  return ""
214 }
215 
216 if {$is_hls_badge} {
217  # HLS mode: locate the component's utilization.txt in bin/. Works for both
218  # mixed projects (bin/<proj>-<ver>/vitis_hls/<comp>/) and pure-HLS projects
219  # (bin/<proj>-<ver>[-<sha>]/<comp>/). Tolerates a trailing "-<sha>" suffix
220  # on the project dir that GetArtifactsAndRename.sh may not have stripped
221  set hls_matches [concat \
222  [glob -nocomplain $repo_path/bin/*/vitis_hls/$hls_component/utilization.txt] \
223  [glob -nocomplain $repo_path/bin/*/$hls_component/utilization.txt]]
224  if {[llength $hls_matches] == 0} {
225  Msg CriticalWarning "Cannot find HLS component '$hls_component' binaries in artifacts"
226  return
227  }
228  # A component name should be unique across the repository. If multiple
229  # matches appear we take the first one and warn
230  if {[llength $hls_matches] > 1} {
231  Msg Warning "Multiple bin dirs contain HLS component '$hls_component'; using [lindex $hls_matches 0]"
232  }
233  set prj_dir [file dirname [lindex $hls_matches 0]]
234  # Derive the version from the enclosing "<project>-<ver>[-<sha>]" dir name
235  set parent $prj_dir
236  if {[file tail [file dirname $parent]] eq "vitis_hls"} {
237  set parent [file dirname [file dirname $parent]]
238  } else {
239  set parent [file dirname $parent]
240  }
241  set ver [extract_ver_from_dir [file tail $parent]]
242  if {$ver eq ""} { set ver "unknown" }
243 } else {
244  # Accept both renamed (bin/<proj>-<ver>) and unrenamed (bin/<proj>-<ver>-<sha>)
245  # layouts so badges still work when GetArtifactsAndRename.sh doesn't strip the
246  # SHA suffix (e.g. for projects that don't emit a .bit/.pof/.bif file)
247  set prj_matches [glob -nocomplain -types d \
248  $repo_path/bin/$project-${ver} \
249  $repo_path/bin/$project-${ver}-*]
250  if {[llength $prj_matches] == 0} {
251  Msg CriticalWarning "Cannot find $project binaries in artifacts"
252  return
253  }
254  # Prefer the exact (renamed) match if present, otherwise take the first
255  set prj_dir [lindex $prj_matches 0]
256  foreach m $prj_matches {
257  if {[file tail $m] eq "$project-${ver}"} { set prj_dir $m; break }
258  }
259 }
260 
261 # Parse a Vivado utilization.txt into a dict { "LUTs" -> percentage, ... }.
262 # The existing Vivado format uses substring matching on the `resources` dict
263 # (keys: "LUTs", "Registers", "Block", "URAM", "DSPs")
264 proc parse_vivado_util {lines resources} {
265  set usage_dict [dict create]
266  foreach line $lines {
267  set str [string map {| ""} $line]
268  set str [string map {"<" ""} $str]
269  set str [string trim $str]
270  set usage [lindex [split $str] end]
271  foreach res [dict keys $resources] {
272  if {[string first $res $str] > -1} {
273  set res_name [dict get $resources $res]
274  dict set usage_dict $res_name $usage
275  }
276  }
277  }
278  return $usage_dict
279 }
280 
281 # Parse an HLS utilization.txt (markdown) into a dict { "LUTs" -> percentage, ... }.
282 # Rows look like "| LUT | 1234 | 230400 | 0.54 |". Implementation tables appear
283 # after Synthesis ones, so the dict naturally ends up with post-P&R numbers
284 proc parse_hls_util {lines} {
285  set usage_dict [dict create]
286  set hls_res_map [dict create LUT "LUTs" FF "FFs" BRAM "BRAM" BRAM_18K "BRAM" URAM "URAM" DSP "DSPs"]
287  foreach line $lines {
288  if {![regexp -- {^\s*\|\s*([A-Za-z_0-9]+)\s*\|\s*\S+\s*\|\s*\S+\s*\|\s*(\S+)\s*\|} $line -> site pct]} {
289  continue
290  }
291  if {[dict exists $hls_res_map $site]} {
292  set res_name [dict get $hls_res_map $site]
293  if {[string is double -strict $pct]} {
294  dict set usage_dict $res_name $pct
295  }
296  }
297  }
298  # Re-emit entries in the canonical Vivado order (LUTs, FFs, BRAM, URAM, DSPs)
299  set ordered [dict create]
300  foreach res_name {LUTs FFs BRAM URAM DSPs} {
301  if {[dict exists $usage_dict $res_name]} {
302  dict set ordered $res_name [dict get $usage_dict $res_name]
303  }
304  }
305  return $ordered
306 }
307 
308 # Emit one timing badge + one set of resource badges for a given "source"
309 # (either Vivado at the project root, or one HLS component in a subfolder).
310 #
311 # util_dir — directory holding utilization.txt + timing_ok/error.txt
312 # badge_suffix — suffix used for filenames and GitLab badge names
313 # label_left — text shown on the left of each badge
314 # is_hls — 1 for HLS badges (adds "HLS" marker on the right side)
315 # util_dict — parsed { resource -> usage% } dict
316 # ver — version string
317 # new_badges_var — name of caller dict to update with generated badge names
318 proc emit_badges {util_dir badge_suffix label_left is_hls util_dict ver new_badges_var} {
319  upvar 1 $new_badges_var new_badges
320 
321  # Timing badge
322  if {[file exists $util_dir/timing_error.txt]} {
323  generate_prj_badge $label_left $ver "#E05D44" "timing-$badge_suffix.svg" $is_hls
324  } elseif {[file exists $util_dir/timing_ok.txt]} {
325  generate_prj_badge $label_left $ver "#006400" "timing-$badge_suffix.svg" $is_hls
326  } else {
327  generate_prj_badge $label_left $ver "#696969" "timing-$badge_suffix.svg" $is_hls
328  }
329  dict set new_badges "timing-$badge_suffix" "timing-$badge_suffix"
330 
331  # Resource badges
332  foreach res [dict keys $util_dict] {
333  set usage [DictGet $util_dict $res]
334  set res_value "$usage\% "
335  if {[expr {$usage < 50.0}]} {
336  generate_res_badge $res $res_value "#90CAF9" "$res-$badge_suffix.svg" $is_hls
337  } elseif {[expr {$usage < 80.0}]} {
338  generate_res_badge $res $res_value "#1565C0" "$res-$badge_suffix.svg" $is_hls
339  } else {
340  generate_res_badge $res $res_value "#0D2B6B" "$res-$badge_suffix.svg" $is_hls
341  }
342  dict set new_badges "$res-$badge_suffix" "$res-$badge_suffix"
343  }
344 }
345 
346 cd $prj_dir
347 
348 set new_badges [dict create]
349 
350 if {$is_hls_badge} {
351  # -------- Single HLS component (opt-in via "hls:<component>") --------
352  # cwd is already the component directory. Badge filename/label uses the
353  # component name alone (component names are expected to be globally
354  # unique across the repo's HLS components)
355  set fp [open utilization.txt]
356  set hls_lines [split [read $fp] "\n"]
357  close $fp
358  set hls_util_dict [parse_hls_util $hls_lines]
359  emit_badges "." $hls_component $hls_component 1 $hls_util_dict $ver new_badges
360 } else {
361  # -------- Vivado (top-level utilization.txt) --------
362  # HLS components inside Vivado bin dirs are NOT auto-discovered anymore;
363  # list them explicitly with "hls:<component>" in HOG_BADGE_PROJECTS
364  set prj_name [string map {/ _} $project]
365  if {[file exists utilization.txt]} {
366  set fp [open utilization.txt]
367  set vivado_lines [split [read $fp] "\n"]
368  close $fp
369  set vivado_util [parse_vivado_util $vivado_lines $resources]
370  emit_badges "." $prj_name $prj_name 0 $vivado_util $ver new_badges
371  }
372 }
373 
374 # -------- Upload all generated badges --------
375 foreach badge_name [dict keys $new_badges] {
376  Msg Info "Uploading badge image $badge_name.svg ...."
377  # tclint-disable-next-line line-length
378  lassign [ExecuteRet {*}$curl_cmd --request POST --header "PRIVATE-TOKEN: ${push_token}" --form "file=@$badge_name.svg" $api_url/projects/$project_id/uploads] ret content
379  set image_url [ParseJSON $content full_path]
380  set image_url $gitlab_url/$image_url
381  if {[dict exists $current_badges $badge_name]} {
382  Msg Info "Badge $badge_name exists, updating it..."
383  set badge_id [DictGet $current_badges $badge_name]
384  Execute curl --header "PRIVATE-TOKEN: $push_token" "$api_url/projects/${project_id}/badges/$badge_id" --request PUT --data "image_url=$image_url"
385  } else {
386  Msg Info "Badge $badge_name does not exist yet. Creating it..."
387  # tclint-disable-next-line line-length
388  Execute curl --header "PRIVATE-TOKEN: $push_token" --request POST --data "link_url=$project_url/-/releases&image_url=$image_url&name=$badge_name" "$api_url/projects/$project_id/badges"
389  }
390 }
391 
392 cd $OldPath