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