Patrick Williams | 73bd93f | 2024-02-20 08:07:48 -0600 | [diff] [blame] | 1 | # |
| 2 | # BitBake Graphical ncurses-based Dependency Explorer |
| 3 | # * Based on the GTK implementation |
| 4 | # * Intended to run on any Linux host |
| 5 | # |
| 6 | # Copyright (C) 2007 Ross Burton |
| 7 | # Copyright (C) 2007 - 2008 Richard Purdie |
| 8 | # Copyright (C) 2022 - 2024 David Reyna |
| 9 | # |
| 10 | # SPDX-License-Identifier: GPL-2.0-only |
| 11 | # |
| 12 | |
| 13 | # |
| 14 | # Execution example: |
Patrick Williams | 3965356 | 2024-03-01 08:54:02 -0600 | [diff] [blame^] | 15 | # $ bitbake -g -u taskexp_ncurses zlib acl |
Patrick Williams | 73bd93f | 2024-02-20 08:07:48 -0600 | [diff] [blame] | 16 | # |
| 17 | # Self-test example (executes a script of GUI actions): |
| 18 | # $ TASK_EXP_UNIT_TEST=1 bitbake -g -u taskexp_ncurses zlib acl |
| 19 | # ... |
| 20 | # $ echo $? |
| 21 | # 0 |
| 22 | # $ TASK_EXP_UNIT_TEST=1 bitbake -g -u taskexp_ncurses zlib acl foo |
| 23 | # ERROR: Nothing PROVIDES 'foo'. Close matches: |
| 24 | # ofono |
| 25 | # $ echo $? |
| 26 | # 1 |
| 27 | # |
| 28 | # Self-test with no terminal example (only tests dependency fetch from bitbake): |
| 29 | # $ TASK_EXP_UNIT_TEST_NOTERM=1 bitbake -g -u taskexp_ncurses quilt |
| 30 | # $ echo $? |
| 31 | # 0 |
| 32 | # |
| 33 | # Features: |
| 34 | # * Ncurses is used for the presentation layer. Only the 'curses' |
| 35 | # library is used (none of the extension libraries), plus only |
| 36 | # one main screen is used (no sub-windows) |
| 37 | # * Uses the 'generateDepTreeEvent' bitbake event to fetch the |
| 38 | # dynamic dependency data based on passed recipes |
| 39 | # * Computes and provides reverse dependencies |
| 40 | # * Supports task sorting on: |
| 41 | # (a) Task dependency order within each recipe |
| 42 | # (b) Pure alphabetical order |
| 43 | # (c) Provisions for third sort order (bitbake order?) |
| 44 | # * The 'Filter' does a "*string*" wildcard filter on tasks in the |
| 45 | # main window, dynamically re-ordering and re-centering the content |
| 46 | # * A 'Print' function exports the selected task or its whole recipe |
| 47 | # task set to the default file "taskdep.txt" |
| 48 | # * Supports a progress bar for bitbake loads and file printing |
| 49 | # * Line art for box drawing supported, ASCII art an alernative |
| 50 | # * No horizontal scrolling support. Selected task's full name |
| 51 | # shown in bottom bar |
| 52 | # * Dynamically catches terminals that are (or become) too small |
| 53 | # * Exception to insure return to normal terminal on errors |
| 54 | # * Debugging support, self test option |
| 55 | # |
| 56 | |
| 57 | import sys |
| 58 | import traceback |
| 59 | import curses |
| 60 | import re |
| 61 | import time |
| 62 | |
| 63 | # Bitbake server support |
| 64 | import threading |
| 65 | from xmlrpc import client |
| 66 | import bb |
| 67 | import bb.event |
| 68 | |
| 69 | # Dependency indexes (depends_model) |
| 70 | (TYPE_DEP, TYPE_RDEP) = (0, 1) |
| 71 | DEPENDS_TYPE = 0 |
| 72 | DEPENDS_TASK = 1 |
| 73 | DEPENDS_DEPS = 2 |
| 74 | # Task indexes (task_list) |
| 75 | TASK_NAME = 0 |
| 76 | TASK_PRIMARY = 1 |
| 77 | TASK_SORT_ALPHA = 2 |
| 78 | TASK_SORT_DEPS = 3 |
| 79 | TASK_SORT_BITBAKE = 4 |
| 80 | # Sort options (default is SORT_DEPS) |
| 81 | SORT_ALPHA = 0 |
| 82 | SORT_DEPS = 1 |
| 83 | SORT_BITBAKE_ENABLE = False # NOTE: future sort |
| 84 | SORT_BITBAKE = 2 |
| 85 | sort_model = SORT_DEPS |
| 86 | # Print options |
| 87 | PRINT_MODEL_1 = 0 |
| 88 | PRINT_MODEL_2 = 1 |
| 89 | print_model = PRINT_MODEL_2 |
| 90 | print_file_name = "taskdep_print.log" |
| 91 | print_file_backup_name = "taskdep_print_backup.log" |
| 92 | is_printed = False |
| 93 | is_filter = False |
| 94 | |
| 95 | # Standard (and backup) key mappings |
| 96 | CHAR_NUL = 0 # Used as self-test nop char |
| 97 | CHAR_BS_H = 8 # Alternate backspace key |
| 98 | CHAR_TAB = 9 |
| 99 | CHAR_RETURN = 10 |
| 100 | CHAR_ESCAPE = 27 |
| 101 | CHAR_UP = ord('{') # Used as self-test ASCII char |
| 102 | CHAR_DOWN = ord('}') # Used as self-test ASCII char |
| 103 | |
| 104 | # Color_pair IDs |
| 105 | CURSES_NORMAL = 0 |
| 106 | CURSES_HIGHLIGHT = 1 |
| 107 | CURSES_WARNING = 2 |
| 108 | |
| 109 | |
| 110 | ################################################# |
| 111 | ### Debugging support |
| 112 | ### |
| 113 | |
| 114 | verbose = False |
| 115 | |
| 116 | # Debug: message display slow-step through display update issues |
| 117 | def alert(msg,screen): |
| 118 | if msg: |
| 119 | screen.addstr(0, 10, '[%-4s]' % msg) |
| 120 | screen.refresh(); |
| 121 | curses.napms(2000) |
| 122 | else: |
| 123 | if do_line_art: |
| 124 | for i in range(10, 24): |
| 125 | screen.addch(0, i, curses.ACS_HLINE) |
| 126 | else: |
| 127 | screen.addstr(0, 10, '-' * 14) |
| 128 | screen.refresh(); |
| 129 | |
| 130 | # Debug: display edge conditions on frame movements |
| 131 | def debug_frame(nbox_ojb): |
| 132 | if verbose: |
| 133 | nbox_ojb.screen.addstr(0, 50, '[I=%2d,O=%2d,S=%3s,H=%2d,M=%4d]' % ( |
| 134 | nbox_ojb.cursor_index, |
| 135 | nbox_ojb.cursor_offset, |
| 136 | nbox_ojb.scroll_offset, |
| 137 | nbox_ojb.inside_height, |
| 138 | len(nbox_ojb.task_list), |
| 139 | )) |
| 140 | nbox_ojb.screen.refresh(); |
| 141 | |
| 142 | # |
| 143 | # Unit test (assumes that 'quilt-native' is always present) |
| 144 | # |
| 145 | |
| 146 | unit_test = os.environ.get('TASK_EXP_UNIT_TEST') |
| 147 | unit_test_cmnds=[ |
| 148 | '# Default selected task in primary box', |
| 149 | 'tst_selected=<TASK>.do_recipe_qa', |
| 150 | '# Default selected task in deps', |
| 151 | 'tst_entry=<TAB>', |
| 152 | 'tst_selected=', |
| 153 | '# Default selected task in rdeps', |
| 154 | 'tst_entry=<TAB>', |
| 155 | 'tst_selected=<TASK>.do_fetch', |
| 156 | "# Test 'select' back to primary box", |
| 157 | 'tst_entry=<CR>', |
| 158 | '#tst_entry=<DOWN>', # optional injected error |
| 159 | 'tst_selected=<TASK>.do_fetch', |
| 160 | '# Check filter', |
| 161 | 'tst_entry=/uilt-nativ/', |
| 162 | 'tst_selected=quilt-native.do_recipe_qa', |
| 163 | '# Check print', |
| 164 | 'tst_entry=p', |
| 165 | 'tst_printed=quilt-native.do_fetch', |
| 166 | '#tst_printed=quilt-foo.do_nothing', # optional injected error |
| 167 | '# Done!', |
| 168 | 'tst_entry=q', |
| 169 | ] |
| 170 | unit_test_idx=0 |
| 171 | unit_test_command_chars='' |
| 172 | unit_test_results=[] |
| 173 | def unit_test_action(active_package): |
| 174 | global unit_test_idx |
| 175 | global unit_test_command_chars |
| 176 | global unit_test_results |
| 177 | ret = CHAR_NUL |
| 178 | if unit_test_command_chars: |
| 179 | ch = unit_test_command_chars[0] |
| 180 | unit_test_command_chars = unit_test_command_chars[1:] |
| 181 | time.sleep(0.5) |
| 182 | ret = ord(ch) |
| 183 | else: |
| 184 | line = unit_test_cmnds[unit_test_idx] |
| 185 | unit_test_idx += 1 |
| 186 | line = re.sub('#.*', '', line).strip() |
| 187 | line = line.replace('<TASK>',active_package.primary[0]) |
| 188 | line = line.replace('<TAB>','\t').replace('<CR>','\n') |
| 189 | line = line.replace('<UP>','{').replace('<DOWN>','}') |
| 190 | if not line: line = 'nop=nop' |
| 191 | cmnd,value = line.split('=') |
| 192 | if cmnd == 'tst_entry': |
| 193 | unit_test_command_chars = value |
| 194 | elif cmnd == 'tst_selected': |
| 195 | active_selected = active_package.get_selected() |
| 196 | if active_selected != value: |
| 197 | unit_test_results.append("ERROR:SELFTEST:expected '%s' but got '%s' (NOTE:bitbake may have changed)" % (value,active_selected)) |
| 198 | ret = ord('Q') |
| 199 | else: |
| 200 | unit_test_results.append("Pass:SELFTEST:found '%s'" % (value)) |
| 201 | elif cmnd == 'tst_printed': |
| 202 | result = os.system('grep %s %s' % (value,print_file_name)) |
| 203 | if result: |
| 204 | unit_test_results.append("ERROR:PRINTTEST:expected '%s' in '%s'" % (value,print_file_name)) |
| 205 | ret = ord('Q') |
| 206 | else: |
| 207 | unit_test_results.append("Pass:PRINTTEST:found '%s'" % (value)) |
| 208 | # Return the action (CHAR_NUL for no action til next round) |
| 209 | return(ret) |
| 210 | |
| 211 | # Unit test without an interative terminal (e.g. ptest) |
| 212 | unit_test_noterm = os.environ.get('TASK_EXP_UNIT_TEST_NOTERM') |
| 213 | |
| 214 | |
| 215 | ################################################# |
| 216 | ### Window frame rendering |
| 217 | ### |
| 218 | ### By default, use the normal line art. Since |
| 219 | ### these extended characters are not ASCII, one |
| 220 | ### must use the ncursus API to render them |
| 221 | ### The alternate ASCII line art set is optionally |
| 222 | ### available via the 'do_line_art' flag |
| 223 | |
| 224 | # By default, render frames using line art |
| 225 | do_line_art = True |
| 226 | |
| 227 | # ASCII render set option |
| 228 | CHAR_HBAR = '-' |
| 229 | CHAR_VBAR = '|' |
| 230 | CHAR_UL_CORNER = '/' |
| 231 | CHAR_UR_CORNER = '\\' |
| 232 | CHAR_LL_CORNER = '\\' |
| 233 | CHAR_LR_CORNER = '/' |
| 234 | |
| 235 | # Box frame drawing with line-art |
| 236 | def line_art_frame(box): |
| 237 | x = box.base_x |
| 238 | y = box.base_y |
| 239 | w = box.width |
| 240 | h = box.height + 1 |
| 241 | |
| 242 | if do_line_art: |
| 243 | for i in range(1, w - 1): |
| 244 | box.screen.addch(y, x + i, curses.ACS_HLINE, box.color) |
| 245 | box.screen.addch(y + h - 1, x + i, curses.ACS_HLINE, box.color) |
| 246 | body_line = "%s" % (' ' * (w - 2)) |
| 247 | for i in range(1, h - 1): |
| 248 | box.screen.addch(y + i, x, curses.ACS_VLINE, box.color) |
| 249 | box.screen.addstr(y + i, x + 1, body_line, box.color) |
| 250 | box.screen.addch(y + i, x + w - 1, curses.ACS_VLINE, box.color) |
| 251 | box.screen.addch(y, x, curses.ACS_ULCORNER, box.color) |
| 252 | box.screen.addch(y, x + w - 1, curses.ACS_URCORNER, box.color) |
| 253 | box.screen.addch(y + h - 1, x, curses.ACS_LLCORNER, box.color) |
| 254 | box.screen.addch(y + h - 1, x + w - 1, curses.ACS_LRCORNER, box.color) |
| 255 | else: |
| 256 | top_line = "%s%s%s" % (CHAR_UL_CORNER,CHAR_HBAR * (w - 2),CHAR_UR_CORNER) |
| 257 | body_line = "%s%s%s" % (CHAR_VBAR,' ' * (w - 2),CHAR_VBAR) |
| 258 | bot_line = "%s%s%s" % (CHAR_UR_CORNER,CHAR_HBAR * (w - 2),CHAR_UL_CORNER) |
| 259 | tag_line = "%s%s%s" % ('[',CHAR_HBAR * (w - 2),']') |
| 260 | # Top bar |
| 261 | box.screen.addstr(y, x, top_line) |
| 262 | # Middle frame |
| 263 | for i in range(1, (h - 1)): |
| 264 | box.screen.addstr(y+i, x, body_line) |
| 265 | # Bottom bar |
| 266 | box.screen.addstr(y + (h - 1), x, bot_line) |
| 267 | |
| 268 | # Connect the separate boxes |
| 269 | def line_art_fixup(box): |
| 270 | if do_line_art: |
| 271 | box.screen.addch(box.base_y+2, box.base_x, curses.ACS_LTEE, box.color) |
| 272 | box.screen.addch(box.base_y+2, box.base_x+box.width-1, curses.ACS_RTEE, box.color) |
| 273 | |
| 274 | |
| 275 | ################################################# |
| 276 | ### Ncurses box object : box frame object to display |
| 277 | ### and manage a sub-window's display elements |
| 278 | ### using basic ncurses |
| 279 | ### |
| 280 | ### Supports: |
| 281 | ### * Frame drawing, content (re)drawing |
| 282 | ### * Content scrolling via ArrowUp, ArrowDn, PgUp, PgDN, |
| 283 | ### * Highlighting for active selected item |
| 284 | ### * Content sorting based on selected sort model |
| 285 | ### |
| 286 | |
| 287 | class NBox(): |
| 288 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 289 | # Box description |
| 290 | self.screen = screen |
| 291 | self.label = label |
| 292 | self.primary = primary |
| 293 | self.color = curses.color_pair(CURSES_NORMAL) if screen else None |
| 294 | # Box boundaries |
| 295 | self.base_x = base_x |
| 296 | self.base_y = base_y |
| 297 | self.width = width |
| 298 | self.height = height |
| 299 | # Cursor/scroll management |
| 300 | self.cursor_enable = False |
| 301 | self.cursor_index = 0 # Absolute offset |
| 302 | self.cursor_offset = 0 # Frame centric offset |
| 303 | self.scroll_offset = 0 # Frame centric offset |
| 304 | # Box specific content |
| 305 | # Format of each entry is [package_name,is_primary_recipe,alpha_sort_key,deps_sort_key] |
| 306 | self.task_list = [] |
| 307 | |
| 308 | @property |
| 309 | def inside_width(self): |
| 310 | return(self.width-2) |
| 311 | |
| 312 | @property |
| 313 | def inside_height(self): |
| 314 | return(self.height-2) |
| 315 | |
| 316 | # Populate the box's content, include the sort mappings and is_primary flag |
| 317 | def task_list_append(self,task_name,dep): |
| 318 | task_sort_alpha = task_name |
| 319 | task_sort_deps = dep.get_dep_sort(task_name) |
| 320 | is_primary = False |
| 321 | for primary in self.primary: |
| 322 | if task_name.startswith(primary+'.'): |
| 323 | is_primary = True |
| 324 | if SORT_BITBAKE_ENABLE: |
| 325 | task_sort_bitbake = dep.get_bb_sort(task_name) |
| 326 | self.task_list.append([task_name,is_primary,task_sort_alpha,task_sort_deps,task_sort_bitbake]) |
| 327 | else: |
| 328 | self.task_list.append([task_name,is_primary,task_sort_alpha,task_sort_deps]) |
| 329 | |
| 330 | def reset(self): |
| 331 | self.task_list = [] |
| 332 | self.cursor_index = 0 # Absolute offset |
| 333 | self.cursor_offset = 0 # Frame centric offset |
| 334 | self.scroll_offset = 0 # Frame centric offset |
| 335 | |
| 336 | # Sort the box's content based on the current sort model |
| 337 | def sort(self): |
| 338 | if SORT_ALPHA == sort_model: |
| 339 | self.task_list.sort(key = lambda x: x[TASK_SORT_ALPHA]) |
| 340 | elif SORT_DEPS == sort_model: |
| 341 | self.task_list.sort(key = lambda x: x[TASK_SORT_DEPS]) |
| 342 | elif SORT_BITBAKE == sort_model: |
| 343 | self.task_list.sort(key = lambda x: x[TASK_SORT_BITBAKE]) |
| 344 | |
| 345 | # The target package list (to hightlight), from the command line |
| 346 | def set_primary(self,primary): |
| 347 | self.primary = primary |
| 348 | |
| 349 | # Draw the box's outside frame |
| 350 | def draw_frame(self): |
| 351 | line_art_frame(self) |
| 352 | # Title |
| 353 | self.screen.addstr(self.base_y, |
| 354 | (self.base_x + (self.width//2))-((len(self.label)+2)//2), |
| 355 | '['+self.label+']') |
| 356 | self.screen.refresh() |
| 357 | |
| 358 | # Draw the box's inside text content |
| 359 | def redraw(self): |
| 360 | task_list_len = len(self.task_list) |
| 361 | # Middle frame |
| 362 | body_line = "%s" % (' ' * (self.inside_width-1) ) |
| 363 | for i in range(0,self.inside_height+1): |
| 364 | if i < (task_list_len + self.scroll_offset): |
| 365 | str_ctl = "%%-%ss" % (self.width-3) |
| 366 | # Safety assert |
| 367 | if (i + self.scroll_offset) >= task_list_len: |
| 368 | alert("REDRAW:%2d,%4d,%4d" % (i,self.scroll_offset,task_list_len),self.screen) |
| 369 | break |
| 370 | |
| 371 | task_obj = self.task_list[i + self.scroll_offset] |
| 372 | task = task_obj[TASK_NAME][:self.inside_width-1] |
| 373 | task_primary = task_obj[TASK_PRIMARY] |
| 374 | |
| 375 | if task_primary: |
| 376 | line = str_ctl % task[:self.inside_width-1] |
| 377 | self.screen.addstr(self.base_y+1+i, self.base_x+2, line, curses.A_BOLD) |
| 378 | else: |
| 379 | line = str_ctl % task[:self.inside_width-1] |
| 380 | self.screen.addstr(self.base_y+1+i, self.base_x+2, line) |
| 381 | else: |
| 382 | line = "%s" % (' ' * (self.inside_width-1) ) |
| 383 | self.screen.addstr(self.base_y+1+i, self.base_x+2, line) |
| 384 | self.screen.refresh() |
| 385 | |
| 386 | # Show the current selected task over the bottom of the frame |
| 387 | def show_selected(self,selected_task): |
| 388 | if not selected_task: |
| 389 | selected_task = self.get_selected() |
| 390 | tag_line = "%s%s%s" % ('[',CHAR_HBAR * (self.width-2),']') |
| 391 | self.screen.addstr(self.base_y + self.height, self.base_x, tag_line) |
| 392 | self.screen.addstr(self.base_y + self.height, |
| 393 | (self.base_x + (self.width//2))-((len(selected_task)+2)//2), |
| 394 | '['+selected_task+']') |
| 395 | self.screen.refresh() |
| 396 | |
| 397 | # Load box with new table of content |
| 398 | def update_content(self,task_list): |
| 399 | self.task_list = task_list |
| 400 | if self.cursor_enable: |
| 401 | cursor_update(turn_on=False) |
| 402 | self.cursor_index = 0 |
| 403 | self.cursor_offset = 0 |
| 404 | self.scroll_offset = 0 |
| 405 | self.redraw() |
| 406 | if self.cursor_enable: |
| 407 | cursor_update(turn_on=True) |
| 408 | |
| 409 | # Manage the box's highlighted task and blinking cursor character |
| 410 | def cursor_on(self,is_on): |
| 411 | self.cursor_enable = is_on |
| 412 | self.cursor_update(is_on) |
| 413 | |
| 414 | # High-light the current pointed package, normal for released packages |
| 415 | def cursor_update(self,turn_on=True): |
| 416 | str_ctl = "%%-%ss" % (self.inside_width-1) |
| 417 | try: |
| 418 | if len(self.task_list): |
| 419 | task_obj = self.task_list[self.cursor_index] |
| 420 | task = task_obj[TASK_NAME][:self.inside_width-1] |
| 421 | task_primary = task_obj[TASK_PRIMARY] |
| 422 | task_font = curses.A_BOLD if task_primary else 0 |
| 423 | else: |
| 424 | task = '' |
| 425 | task_font = 0 |
| 426 | except Exception as e: |
| 427 | alert("CURSOR_UPDATE:%s" % (e),self.screen) |
| 428 | return |
| 429 | if turn_on: |
| 430 | self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+1,">", curses.color_pair(CURSES_HIGHLIGHT) | curses.A_BLINK) |
| 431 | self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+2,str_ctl % task, curses.color_pair(CURSES_HIGHLIGHT) | task_font) |
| 432 | else: |
| 433 | self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+1," ") |
| 434 | self.screen.addstr(self.base_y+1+self.cursor_offset,self.base_x+2,str_ctl % task, task_font) |
| 435 | |
| 436 | # Down arrow |
| 437 | def line_down(self): |
| 438 | if len(self.task_list) <= (self.cursor_index+1): |
| 439 | return |
| 440 | self.cursor_update(turn_on=False) |
| 441 | self.cursor_index += 1 |
| 442 | self.cursor_offset += 1 |
| 443 | if self.cursor_offset > (self.inside_height): |
| 444 | self.cursor_offset -= 1 |
| 445 | self.scroll_offset += 1 |
| 446 | self.redraw() |
| 447 | self.cursor_update(turn_on=True) |
| 448 | debug_frame(self) |
| 449 | |
| 450 | # Up arrow |
| 451 | def line_up(self): |
| 452 | if 0 > (self.cursor_index-1): |
| 453 | return |
| 454 | self.cursor_update(turn_on=False) |
| 455 | self.cursor_index -= 1 |
| 456 | self.cursor_offset -= 1 |
| 457 | if self.cursor_offset < 0: |
| 458 | self.cursor_offset += 1 |
| 459 | self.scroll_offset -= 1 |
| 460 | self.redraw() |
| 461 | self.cursor_update(turn_on=True) |
| 462 | debug_frame(self) |
| 463 | |
| 464 | # Page down |
| 465 | def page_down(self): |
| 466 | max_task = len(self.task_list)-1 |
| 467 | if max_task < self.inside_height: |
| 468 | return |
| 469 | self.cursor_update(turn_on=False) |
| 470 | self.cursor_index += 10 |
| 471 | self.cursor_index = min(self.cursor_index,max_task) |
| 472 | self.cursor_offset = min(self.inside_height,self.cursor_index) |
| 473 | self.scroll_offset = self.cursor_index - self.cursor_offset |
| 474 | self.redraw() |
| 475 | self.cursor_update(turn_on=True) |
| 476 | debug_frame(self) |
| 477 | |
| 478 | # Page up |
| 479 | def page_up(self): |
| 480 | max_task = len(self.task_list)-1 |
| 481 | if max_task < self.inside_height: |
| 482 | return |
| 483 | self.cursor_update(turn_on=False) |
| 484 | self.cursor_index -= 10 |
| 485 | self.cursor_index = max(self.cursor_index,0) |
| 486 | self.cursor_offset = max(0, self.inside_height - (max_task - self.cursor_index)) |
| 487 | self.scroll_offset = self.cursor_index - self.cursor_offset |
| 488 | self.redraw() |
| 489 | self.cursor_update(turn_on=True) |
| 490 | debug_frame(self) |
| 491 | |
| 492 | # Return the currently selected task name for this box |
| 493 | def get_selected(self): |
| 494 | if self.task_list: |
| 495 | return(self.task_list[self.cursor_index][TASK_NAME]) |
| 496 | else: |
| 497 | return('') |
| 498 | |
| 499 | ################################################# |
| 500 | ### The helper sub-windows |
| 501 | ### |
| 502 | |
| 503 | # Show persistent help at the top of the screen |
| 504 | class HelpBarView(NBox): |
| 505 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 506 | super(HelpBarView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 507 | |
| 508 | def show_help(self,show): |
| 509 | self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.inside_width)) |
| 510 | if show: |
| 511 | help = "Help='?' Filter='/' NextBox=<Tab> Select=<Enter> Print='p','P' Quit='q'" |
| 512 | bar_size = self.inside_width - 5 - len(help) |
| 513 | self.screen.addstr(self.base_y,self.base_x+((self.inside_width-len(help))//2), help) |
| 514 | self.screen.refresh() |
| 515 | |
| 516 | # Pop up a detailed Help box |
| 517 | class HelpBoxView(NBox): |
| 518 | def __init__(self, screen, label, primary, base_x, base_y, width, height, dep): |
| 519 | super(HelpBoxView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 520 | self.x_pos = 0 |
| 521 | self.y_pos = 0 |
| 522 | self.dep = dep |
| 523 | |
| 524 | # Instantial the pop-up help box |
| 525 | def show_help(self,show): |
| 526 | self.x_pos = self.base_x + 4 |
| 527 | self.y_pos = self.base_y + 2 |
| 528 | |
| 529 | def add_line(line): |
| 530 | if line: |
| 531 | self.screen.addstr(self.y_pos,self.x_pos,line) |
| 532 | self.y_pos += 1 |
| 533 | |
| 534 | # Gather some statisics |
| 535 | dep_count = 0 |
| 536 | rdep_count = 0 |
| 537 | for task_obj in self.dep.depends_model: |
| 538 | if TYPE_DEP == task_obj[DEPENDS_TYPE]: |
| 539 | dep_count += 1 |
| 540 | elif TYPE_RDEP == task_obj[DEPENDS_TYPE]: |
| 541 | rdep_count += 1 |
| 542 | |
| 543 | self.draw_frame() |
| 544 | line_art_fixup(self.dep) |
| 545 | add_line("Quit : 'q' ") |
| 546 | add_line("Filter task names : '/'") |
| 547 | add_line("Tab to next box : <Tab>") |
| 548 | add_line("Select a task : <Enter>") |
| 549 | add_line("Print task's deps : 'p'") |
| 550 | add_line("Print recipe's deps : 'P'") |
| 551 | add_line(" -> '%s'" % print_file_name) |
| 552 | add_line("Sort toggle : 's'") |
| 553 | add_line(" %s Recipe inner-depends order" % ('->' if (SORT_DEPS == sort_model) else '- ')) |
| 554 | add_line(" %s Alpha-numeric order" % ('->' if (SORT_ALPHA == sort_model) else '- ')) |
| 555 | if SORT_BITBAKE_ENABLE: |
| 556 | add_line(" %s Bitbake order" % ('->' if (TASK_SORT_BITBAKE == sort_model) else '- ')) |
| 557 | add_line("Alternate backspace : <CTRL-H>") |
| 558 | add_line("") |
| 559 | add_line("Primary recipes = %s" % ','.join(self.primary)) |
| 560 | add_line("Task count = %4d" % len(self.dep.pkg_model)) |
| 561 | add_line("Deps count = %4d" % dep_count) |
| 562 | add_line("RDeps count = %4d" % rdep_count) |
| 563 | add_line("") |
| 564 | self.screen.addstr(self.y_pos,self.x_pos+7,"<Press any key>", curses.color_pair(CURSES_HIGHLIGHT)) |
| 565 | self.screen.refresh() |
| 566 | c = self.screen.getch() |
| 567 | |
| 568 | # Show a progress bar |
| 569 | class ProgressView(NBox): |
| 570 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 571 | super(ProgressView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 572 | |
| 573 | def progress(self,title,current,max): |
| 574 | if title: |
| 575 | self.label = title |
| 576 | else: |
| 577 | title = self.label |
| 578 | if max <=0: max = 10 |
| 579 | bar_size = self.width - 7 - len(title) |
| 580 | bar_done = int( (float(current)/float(max)) * float(bar_size) ) |
| 581 | self.screen.addstr(self.base_y,self.base_x, " %s:[%s%s]" % (title,'*' * bar_done,' ' * (bar_size-bar_done))) |
| 582 | self.screen.refresh() |
| 583 | return(current+1) |
| 584 | |
| 585 | def clear(self): |
| 586 | self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.width)) |
| 587 | self.screen.refresh() |
| 588 | |
| 589 | # Implement a task filter bar |
| 590 | class FilterView(NBox): |
| 591 | SEARCH_NOP = 0 |
| 592 | SEARCH_GO = 1 |
| 593 | SEARCH_CANCEL = 2 |
| 594 | |
| 595 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 596 | super(FilterView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 597 | self.do_show = False |
| 598 | self.filter_str = "" |
| 599 | |
| 600 | def clear(self,enable_show=True): |
| 601 | self.filter_str = "" |
| 602 | |
| 603 | def show(self,enable_show=True): |
| 604 | self.do_show = enable_show |
| 605 | if self.do_show: |
| 606 | self.screen.addstr(self.base_y,self.base_x, "[ Filter: %-25s ] '/'=cancel, format='abc' " % self.filter_str[0:25]) |
| 607 | else: |
| 608 | self.screen.addstr(self.base_y,self.base_x, "%s" % (' ' * self.width)) |
| 609 | self.screen.refresh() |
| 610 | |
| 611 | def show_prompt(self): |
| 612 | self.screen.addstr(self.base_y,self.base_x + 10 + len(self.filter_str), " ") |
| 613 | self.screen.addstr(self.base_y,self.base_x + 10 + len(self.filter_str), "") |
| 614 | |
| 615 | # Keys specific to the filter box (start/stop filter keys are in the main loop) |
| 616 | def input(self,c,ch): |
| 617 | ret = self.SEARCH_GO |
| 618 | if c in (curses.KEY_BACKSPACE,CHAR_BS_H): |
| 619 | # Backspace |
| 620 | if self.filter_str: |
| 621 | self.filter_str = self.filter_str[0:-1] |
| 622 | self.show() |
| 623 | elif ((ch >= 'a') and (ch <= 'z')) or ((ch >= 'A') and (ch <= 'Z')) or ((ch >= '0') and (ch <= '9')) or (ch in (' ','_','.','-')): |
| 624 | # The isalnum() acts strangly with keypad(True), so explicit bounds |
| 625 | self.filter_str += ch |
| 626 | self.show() |
| 627 | else: |
| 628 | ret = self.SEARCH_NOP |
| 629 | return(ret) |
| 630 | |
| 631 | |
| 632 | ################################################# |
| 633 | ### The primary dependency windows |
| 634 | ### |
| 635 | |
| 636 | # The main list of package tasks |
| 637 | class PackageView(NBox): |
| 638 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 639 | super(PackageView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 640 | |
| 641 | # Find and verticaly center a selected task (from filter or from dependent box) |
| 642 | # The 'task_filter_str' can be a full or a partial (filter) task name |
| 643 | def find(self,task_filter_str): |
| 644 | found = False |
| 645 | max = self.height-2 |
| 646 | if not task_filter_str: |
| 647 | return(found) |
| 648 | for i,task_obj in enumerate(self.task_list): |
| 649 | task = task_obj[TASK_NAME] |
| 650 | if task.startswith(task_filter_str): |
| 651 | self.cursor_on(False) |
| 652 | self.cursor_index = i |
| 653 | |
| 654 | # Position selected at vertical center |
| 655 | vcenter = self.inside_height // 2 |
| 656 | if self.cursor_index <= vcenter: |
| 657 | self.scroll_offset = 0 |
| 658 | self.cursor_offset = self.cursor_index |
| 659 | elif self.cursor_index >= (len(self.task_list) - vcenter - 1): |
| 660 | self.cursor_offset = self.inside_height-1 |
| 661 | self.scroll_offset = self.cursor_index - self.cursor_offset |
| 662 | else: |
| 663 | self.cursor_offset = vcenter |
| 664 | self.scroll_offset = self.cursor_index - self.cursor_offset |
| 665 | |
| 666 | self.redraw() |
| 667 | self.cursor_on(True) |
| 668 | found = True |
| 669 | break |
| 670 | return(found) |
| 671 | |
| 672 | # The view of dependent packages |
| 673 | class PackageDepView(NBox): |
| 674 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 675 | super(PackageDepView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 676 | |
| 677 | # The view of reverse-dependent packages |
| 678 | class PackageReverseDepView(NBox): |
| 679 | def __init__(self, screen, label, primary, base_x, base_y, width, height): |
| 680 | super(PackageReverseDepView, self).__init__(screen, label, primary, base_x, base_y, width, height) |
| 681 | |
| 682 | |
| 683 | ################################################# |
| 684 | ### DepExplorer : The parent frame and object |
| 685 | ### |
| 686 | |
| 687 | class DepExplorer(NBox): |
| 688 | def __init__(self,screen): |
| 689 | title = "Task Dependency Explorer" |
| 690 | super(DepExplorer, self).__init__(screen, 'Task Dependency Explorer','',0,0,80,23) |
| 691 | |
| 692 | self.screen = screen |
| 693 | self.pkg_model = [] |
| 694 | self.depends_model = [] |
| 695 | self.dep_sort_map = {} |
| 696 | self.bb_sort_map = {} |
| 697 | self.filter_str = '' |
| 698 | self.filter_prev = 'deadbeef' |
| 699 | |
| 700 | if self.screen: |
| 701 | self.help_bar_view = HelpBarView(screen, "Help",'',1,1,79,1) |
| 702 | self.help_box_view = HelpBoxView(screen, "Help",'',0,2,40,20,self) |
| 703 | self.progress_view = ProgressView(screen, "Progress",'',2,1,76,1) |
| 704 | self.filter_view = FilterView(screen, "Filter",'',2,1,76,1) |
| 705 | self.package_view = PackageView(screen, "Package",'alpha', 0,2,40,20) |
| 706 | self.dep_view = PackageDepView(screen, "Dependencies",'beta',40,2,40,10) |
| 707 | self.reverse_view = PackageReverseDepView(screen, "Dependent Tasks",'gamma',40,13,40,9) |
| 708 | self.draw_frames() |
| 709 | |
| 710 | # Draw this main window's frame and all sub-windows |
| 711 | def draw_frames(self): |
| 712 | self.draw_frame() |
| 713 | self.package_view.draw_frame() |
| 714 | self.dep_view.draw_frame() |
| 715 | self.reverse_view.draw_frame() |
| 716 | if is_filter: |
| 717 | self.filter_view.show(True) |
| 718 | self.filter_view.show_prompt() |
| 719 | else: |
| 720 | self.help_bar_view.show_help(True) |
| 721 | self.package_view.redraw() |
| 722 | self.dep_view.redraw() |
| 723 | self.reverse_view.redraw() |
| 724 | self.show_selected(self.package_view.get_selected()) |
| 725 | line_art_fixup(self) |
| 726 | |
| 727 | # Parse the bitbake dependency event object |
| 728 | def parse(self, depgraph): |
| 729 | for task in depgraph["tdepends"]: |
| 730 | self.pkg_model.insert(0, task) |
| 731 | for depend in depgraph["tdepends"][task]: |
| 732 | self.depends_model.insert (0, (TYPE_DEP, task, depend)) |
| 733 | self.depends_model.insert (0, (TYPE_RDEP, depend, task)) |
| 734 | if self.screen: |
| 735 | self.dep_sort_prep() |
| 736 | |
| 737 | # Prepare the dependency sort order keys |
| 738 | # This method creates sort keys per recipe tasks in |
| 739 | # the order of each recipe's internal dependecies |
| 740 | # Method: |
| 741 | # Filter the tasks in dep order in dep_sort_map = {} |
| 742 | # (a) Find a task that has no dependecies |
| 743 | # Ignore non-recipe specific tasks |
| 744 | # (b) Add it to the sort mapping dict with |
| 745 | # key of "<task_group>_<order>" |
| 746 | # (c) Remove it as a dependency from the other tasks |
| 747 | # (d) Repeat till all tasks are mapped |
| 748 | # Use placeholders to insure each sub-dict is instantiated |
| 749 | def dep_sort_prep(self): |
| 750 | self.progress_view.progress('DepSort',0,4) |
| 751 | # Init the task base entries |
| 752 | self.progress_view.progress('DepSort',1,4) |
| 753 | dep_table = {} |
| 754 | bb_index = 0 |
| 755 | for task in self.pkg_model: |
| 756 | # First define the incoming bitbake sort order |
| 757 | self.bb_sort_map[task] = "%04d" % (bb_index) |
| 758 | bb_index += 1 |
| 759 | task_group = task[0:task.find('.')] |
| 760 | if task_group not in dep_table: |
| 761 | dep_table[task_group] = {} |
| 762 | dep_table[task_group]['-'] = {} # Placeholder |
| 763 | if task not in dep_table[task_group]: |
| 764 | dep_table[task_group][task] = {} |
| 765 | dep_table[task_group][task]['-'] = {} # Placeholder |
| 766 | # Add the task dependecy entries |
| 767 | self.progress_view.progress('DepSort',2,4) |
| 768 | for task_obj in self.depends_model: |
| 769 | if task_obj[DEPENDS_TYPE] != TYPE_DEP: |
| 770 | continue |
| 771 | task = task_obj[DEPENDS_TASK] |
| 772 | task_dep = task_obj[DEPENDS_DEPS] |
| 773 | task_group = task[0:task.find('.')] |
| 774 | # Only track depends within same group |
| 775 | if task_dep.startswith(task_group+'.'): |
| 776 | dep_table[task_group][task][task_dep] = 1 |
| 777 | self.progress_view.progress('DepSort',3,4) |
| 778 | for task_group in dep_table: |
| 779 | dep_index = 0 |
| 780 | # Whittle down the tasks of each group |
| 781 | this_pass = 1 |
| 782 | do_loop = True |
| 783 | while (len(dep_table[task_group]) > 1) and do_loop: |
| 784 | this_pass += 1 |
| 785 | is_change = False |
| 786 | delete_list = [] |
| 787 | for task in dep_table[task_group]: |
| 788 | if '-' == task: |
| 789 | continue |
| 790 | if 1 == len(dep_table[task_group][task]): |
| 791 | is_change = True |
| 792 | # No more deps, so collect this task... |
| 793 | self.dep_sort_map[task] = "%s_%04d" % (task_group,dep_index) |
| 794 | dep_index += 1 |
| 795 | # ... remove it from other lists as resolved ... |
| 796 | for dep_task in dep_table[task_group]: |
| 797 | if task in dep_table[task_group][dep_task]: |
| 798 | del dep_table[task_group][dep_task][task] |
| 799 | # ... and remove it from from the task group |
| 800 | delete_list.append(task) |
| 801 | for task in delete_list: |
| 802 | del dep_table[task_group][task] |
| 803 | if not is_change: |
| 804 | alert("ERROR:DEP_SIEVE_NO_CHANGE:%s" % task_group,self.screen) |
| 805 | do_loop = False |
| 806 | continue |
| 807 | self.progress_view.progress('',4,4) |
| 808 | self.progress_view.clear() |
| 809 | self.help_bar_view.show_help(True) |
| 810 | if len(self.dep_sort_map) != len(self.pkg_model): |
| 811 | alert("ErrorDepSort:%d/%d" % (len(self.dep_sort_map),len(self.pkg_model)),self.screen) |
| 812 | |
| 813 | # Look up a dep sort order key |
| 814 | def get_dep_sort(self,key): |
| 815 | if key in self.dep_sort_map: |
| 816 | return(self.dep_sort_map[key]) |
| 817 | else: |
| 818 | return(key) |
| 819 | |
| 820 | # Look up a bitbake sort order key |
| 821 | def get_bb_sort(self,key): |
| 822 | if key in self.bb_sort_map: |
| 823 | return(self.bb_sort_map[key]) |
| 824 | else: |
| 825 | return(key) |
| 826 | |
| 827 | # Find the selected package in the main frame, update the dependency frames content accordingly |
| 828 | def select(self, package_name, only_update_dependents=False): |
| 829 | if not package_name: |
| 830 | package_name = self.package_view.get_selected() |
| 831 | # alert("SELECT:%s:" % package_name,self.screen) |
| 832 | |
| 833 | if self.filter_str != self.filter_prev: |
| 834 | self.package_view.cursor_on(False) |
| 835 | # Fill of the main package task list using new filter |
| 836 | self.package_view.task_list = [] |
| 837 | for package in self.pkg_model: |
| 838 | if self.filter_str: |
| 839 | if self.filter_str in package: |
| 840 | self.package_view.task_list_append(package,self) |
| 841 | else: |
| 842 | self.package_view.task_list_append(package,self) |
| 843 | self.package_view.sort() |
| 844 | self.filter_prev = self.filter_str |
| 845 | |
| 846 | # Old position is lost, assert new position of previous task (if still filtered in) |
| 847 | self.package_view.cursor_index = 0 |
| 848 | self.package_view.cursor_offset = 0 |
| 849 | self.package_view.scroll_offset = 0 |
| 850 | self.package_view.redraw() |
| 851 | self.package_view.cursor_on(True) |
| 852 | |
| 853 | # Make sure the selected package is in view, with implicit redraw() |
| 854 | if (not only_update_dependents): |
| 855 | self.package_view.find(package_name) |
| 856 | # In case selected name change (i.e. filter removed previous) |
| 857 | package_name = self.package_view.get_selected() |
| 858 | |
| 859 | # Filter the package's dependent list to the dependent view |
| 860 | self.dep_view.reset() |
| 861 | for package_def in self.depends_model: |
| 862 | if (package_def[DEPENDS_TYPE] == TYPE_DEP) and (package_def[DEPENDS_TASK] == package_name): |
| 863 | self.dep_view.task_list_append(package_def[DEPENDS_DEPS],self) |
| 864 | self.dep_view.sort() |
| 865 | self.dep_view.redraw() |
| 866 | # Filter the package's dependent list to the reverse dependent view |
| 867 | self.reverse_view.reset() |
| 868 | for package_def in self.depends_model: |
| 869 | if (package_def[DEPENDS_TYPE] == TYPE_RDEP) and (package_def[DEPENDS_TASK] == package_name): |
| 870 | self.reverse_view.task_list_append(package_def[DEPENDS_DEPS],self) |
| 871 | self.reverse_view.sort() |
| 872 | self.reverse_view.redraw() |
| 873 | self.show_selected(package_name) |
| 874 | self.screen.refresh() |
| 875 | |
| 876 | # The print-to-file method |
| 877 | def print_deps(self,whole_group=False): |
| 878 | global is_printed |
| 879 | # Print the selected deptree(s) to a file |
| 880 | if not is_printed: |
| 881 | try: |
| 882 | # Move to backup any exiting file before first write |
| 883 | if os.path.isfile(print_file_name): |
| 884 | os.system('mv -f %s %s' % (print_file_name,print_file_backup_name)) |
| 885 | except Exception as e: |
| 886 | alert(e,self.screen) |
| 887 | alert('',self.screen) |
| 888 | print_list = [] |
| 889 | selected_task = self.package_view.get_selected() |
| 890 | if not selected_task: |
| 891 | return |
| 892 | if not whole_group: |
| 893 | print_list.append(selected_task) |
| 894 | else: |
| 895 | # Use the presorted task_group order from 'package_view' |
| 896 | task_group = selected_task[0:selected_task.find('.')+1] |
| 897 | for task_obj in self.package_view.task_list: |
| 898 | task = task_obj[TASK_NAME] |
| 899 | if task.startswith(task_group): |
| 900 | print_list.append(task) |
| 901 | with open(print_file_name, "a") as fd: |
| 902 | print_max = len(print_list) |
| 903 | print_count = 1 |
| 904 | self.progress_view.progress('Write "%s"' % print_file_name,0,print_max) |
| 905 | for task in print_list: |
| 906 | print_count = self.progress_view.progress('',print_count,print_max) |
| 907 | self.select(task) |
| 908 | self.screen.refresh(); |
| 909 | # Utilize the current print output model |
| 910 | if print_model == PRINT_MODEL_1: |
| 911 | print("=== Dependendency Snapshot ===",file=fd) |
| 912 | print(" = Package =",file=fd) |
| 913 | print(' '+task,file=fd) |
| 914 | # Fill in the matching dependencies |
| 915 | print(" = Dependencies =",file=fd) |
| 916 | for task_obj in self.dep_view.task_list: |
| 917 | print(' '+ task_obj[TASK_NAME],file=fd) |
| 918 | print(" = Dependent Tasks =",file=fd) |
| 919 | for task_obj in self.reverse_view.task_list: |
| 920 | print(' '+ task_obj[TASK_NAME],file=fd) |
| 921 | if print_model == PRINT_MODEL_2: |
| 922 | print("=== Dependendency Snapshot ===",file=fd) |
| 923 | dep_count = len(self.dep_view.task_list) - 1 |
| 924 | for i,task_obj in enumerate(self.dep_view.task_list): |
| 925 | print('%s%s' % ("Dep =" if (i==dep_count) else " ",task_obj[TASK_NAME]),file=fd) |
| 926 | if not self.dep_view.task_list: |
| 927 | print('Dep =',file=fd) |
| 928 | print("Package=%s" % task,file=fd) |
| 929 | for i,task_obj in enumerate(self.reverse_view.task_list): |
| 930 | print('%s%s' % ("RDep =" if (i==0) else " ",task_obj[TASK_NAME]),file=fd) |
| 931 | if not self.reverse_view.task_list: |
| 932 | print('RDep =',file=fd) |
| 933 | curses.napms(2000) |
| 934 | self.progress_view.clear() |
| 935 | self.help_bar_view.show_help(True) |
| 936 | print('',file=fd) |
| 937 | # Restore display to original selected task |
| 938 | self.select(selected_task) |
| 939 | is_printed = True |
| 940 | |
| 941 | ################################################# |
| 942 | ### Load bitbake data |
| 943 | ### |
| 944 | |
| 945 | def bitbake_load(server, eventHandler, params, dep, curses_off, screen): |
| 946 | global bar_len_old |
| 947 | bar_len_old = 0 |
| 948 | |
| 949 | # Support no screen |
| 950 | def progress(msg,count,max): |
| 951 | global bar_len_old |
| 952 | if screen: |
| 953 | dep.progress_view.progress(msg,count,max) |
| 954 | else: |
| 955 | if msg: |
| 956 | if bar_len_old: |
| 957 | bar_len_old = 0 |
| 958 | print("\n") |
| 959 | print(f"{msg}: ({count} of {max})") |
| 960 | else: |
| 961 | bar_len = int((count*40)/max) |
| 962 | if bar_len_old != bar_len: |
| 963 | print(f"{'*' * (bar_len-bar_len_old)}",end='',flush=True) |
| 964 | bar_len_old = bar_len |
| 965 | def clear(): |
| 966 | if screen: |
| 967 | dep.progress_view.clear() |
| 968 | def clear_curses(screen): |
| 969 | if screen: |
| 970 | curses_off(screen) |
| 971 | |
| 972 | # |
| 973 | # Trigger bitbake "generateDepTreeEvent" |
| 974 | # |
| 975 | |
| 976 | cmdline = '' |
| 977 | try: |
| 978 | params.updateToServer(server, os.environ.copy()) |
| 979 | params.updateFromServer(server) |
| 980 | cmdline = params.parseActions() |
| 981 | if not cmdline: |
| 982 | clear_curses(screen) |
| 983 | print("ERROR: nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") |
| 984 | return 1,cmdline |
| 985 | if 'msg' in cmdline and cmdline['msg']: |
| 986 | clear_curses(screen) |
| 987 | print('ERROR: ' + cmdline['msg']) |
| 988 | return 1,cmdline |
| 989 | cmdline = cmdline['action'] |
| 990 | if not cmdline or cmdline[0] != "generateDotGraph": |
| 991 | clear_curses(screen) |
| 992 | print("ERROR: This UI requires the -g option") |
| 993 | return 1,cmdline |
| 994 | ret, error = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]]) |
| 995 | if error: |
| 996 | clear_curses(screen) |
| 997 | print("ERROR: running command '%s': %s" % (cmdline, error)) |
| 998 | return 1,cmdline |
| 999 | elif not ret: |
| 1000 | clear_curses(screen) |
| 1001 | print("ERROR: running command '%s': returned %s" % (cmdline, ret)) |
| 1002 | return 1,cmdline |
| 1003 | except client.Fault as x: |
| 1004 | clear_curses(screen) |
| 1005 | print("ERROR: XMLRPC Fault getting commandline:\n %s" % x) |
| 1006 | return 1,cmdline |
| 1007 | except Exception as e: |
| 1008 | clear_curses(screen) |
| 1009 | print("ERROR: in startup:\n %s" % traceback.format_exc()) |
| 1010 | return 1,cmdline |
| 1011 | |
| 1012 | # |
| 1013 | # Receive data from bitbake |
| 1014 | # |
| 1015 | |
| 1016 | progress_total = 0 |
| 1017 | load_bitbake = True |
| 1018 | quit = False |
| 1019 | try: |
| 1020 | while load_bitbake: |
| 1021 | try: |
| 1022 | event = eventHandler.waitEvent(0.25) |
| 1023 | if quit: |
| 1024 | _, error = server.runCommand(["stateForceShutdown"]) |
| 1025 | clear_curses(screen) |
| 1026 | if error: |
| 1027 | print('Unable to cleanly stop: %s' % error) |
| 1028 | break |
| 1029 | |
| 1030 | if event is None: |
| 1031 | continue |
| 1032 | |
| 1033 | if isinstance(event, bb.event.CacheLoadStarted): |
| 1034 | progress_total = event.total |
| 1035 | progress('Loading Cache',0,progress_total) |
| 1036 | continue |
| 1037 | |
| 1038 | if isinstance(event, bb.event.CacheLoadProgress): |
| 1039 | x = event.current |
| 1040 | progress('',x,progress_total) |
| 1041 | continue |
| 1042 | |
| 1043 | if isinstance(event, bb.event.CacheLoadCompleted): |
| 1044 | clear() |
| 1045 | progress('Bitbake... ',1,2) |
| 1046 | continue |
| 1047 | |
| 1048 | if isinstance(event, bb.event.ParseStarted): |
| 1049 | progress_total = event.total |
| 1050 | progress('Processing recipes',0,progress_total) |
| 1051 | if progress_total == 0: |
| 1052 | continue |
| 1053 | |
| 1054 | if isinstance(event, bb.event.ParseProgress): |
| 1055 | x = event.current |
| 1056 | progress('',x,progress_total) |
| 1057 | continue |
| 1058 | |
| 1059 | if isinstance(event, bb.event.ParseCompleted): |
| 1060 | progress('Generating dependency tree',0,3) |
| 1061 | continue |
| 1062 | |
| 1063 | if isinstance(event, bb.event.DepTreeGenerated): |
| 1064 | progress('Generating dependency tree',1,3) |
| 1065 | dep.parse(event._depgraph) |
| 1066 | progress('Generating dependency tree',2,3) |
| 1067 | |
| 1068 | if isinstance(event, bb.command.CommandCompleted): |
| 1069 | load_bitbake = False |
| 1070 | progress('Generating dependency tree',3,3) |
| 1071 | clear() |
| 1072 | if screen: |
| 1073 | dep.help_bar_view.show_help(True) |
| 1074 | continue |
| 1075 | |
| 1076 | if isinstance(event, bb.event.NoProvider): |
| 1077 | clear_curses(screen) |
| 1078 | print('ERROR: %s' % event) |
| 1079 | |
| 1080 | _, error = server.runCommand(["stateShutdown"]) |
| 1081 | if error: |
| 1082 | print('ERROR: Unable to cleanly shutdown: %s' % error) |
| 1083 | return 1,cmdline |
| 1084 | |
| 1085 | if isinstance(event, bb.command.CommandFailed): |
| 1086 | clear_curses(screen) |
| 1087 | print('ERROR: ' + str(event)) |
| 1088 | return event.exitcode,cmdline |
| 1089 | |
| 1090 | if isinstance(event, bb.command.CommandExit): |
| 1091 | clear_curses(screen) |
| 1092 | return event.exitcode,cmdline |
| 1093 | |
| 1094 | if isinstance(event, bb.cooker.CookerExit): |
| 1095 | break |
| 1096 | |
| 1097 | continue |
| 1098 | except EnvironmentError as ioerror: |
| 1099 | # ignore interrupted io |
| 1100 | if ioerror.args[0] == 4: |
| 1101 | pass |
| 1102 | except KeyboardInterrupt: |
| 1103 | if shutdown == 2: |
| 1104 | clear_curses(screen) |
| 1105 | print("\nThird Keyboard Interrupt, exit.\n") |
| 1106 | break |
| 1107 | if shutdown == 1: |
| 1108 | clear_curses(screen) |
| 1109 | print("\nSecond Keyboard Interrupt, stopping...\n") |
| 1110 | _, error = server.runCommand(["stateForceShutdown"]) |
| 1111 | if error: |
| 1112 | print('Unable to cleanly stop: %s' % error) |
| 1113 | if shutdown == 0: |
| 1114 | clear_curses(screen) |
| 1115 | print("\nKeyboard Interrupt, closing down...\n") |
| 1116 | _, error = server.runCommand(["stateShutdown"]) |
| 1117 | if error: |
| 1118 | print('Unable to cleanly shutdown: %s' % error) |
| 1119 | shutdown = shutdown + 1 |
| 1120 | pass |
| 1121 | except Exception as e: |
| 1122 | # Safe exit on error |
| 1123 | clear_curses(screen) |
| 1124 | print("Exception : %s" % e) |
| 1125 | print("Exception in startup:\n %s" % traceback.format_exc()) |
| 1126 | |
| 1127 | return 0,cmdline |
| 1128 | |
| 1129 | ################################################# |
| 1130 | ### main |
| 1131 | ### |
| 1132 | |
| 1133 | SCREEN_COL_MIN = 83 |
| 1134 | SCREEN_ROW_MIN = 26 |
| 1135 | |
| 1136 | def main(server, eventHandler, params): |
| 1137 | global verbose |
| 1138 | global sort_model |
| 1139 | global print_model |
| 1140 | global is_printed |
| 1141 | global is_filter |
| 1142 | global screen_too_small |
| 1143 | |
| 1144 | shutdown = 0 |
| 1145 | screen_too_small = False |
| 1146 | quit = False |
| 1147 | |
| 1148 | # Unit test with no terminal? |
| 1149 | if unit_test_noterm: |
| 1150 | # Load bitbake, test that there is valid dependency data, then exit |
| 1151 | screen = None |
| 1152 | print("* UNIT TEST:START") |
| 1153 | dep = DepExplorer(screen) |
| 1154 | print("* UNIT TEST:BITBAKE FETCH") |
| 1155 | ret,cmdline = bitbake_load(server, eventHandler, params, dep, None, screen) |
| 1156 | if ret: |
| 1157 | print("* UNIT TEST: BITBAKE FAILED") |
| 1158 | return ret |
| 1159 | # Test the acquired dependency data |
| 1160 | quilt_native_deps = 0 |
| 1161 | quilt_native_rdeps = 0 |
| 1162 | quilt_deps = 0 |
| 1163 | quilt_rdeps = 0 |
| 1164 | for i,task_obj in enumerate(dep.depends_model): |
| 1165 | if TYPE_DEP == task_obj[0]: |
| 1166 | task = task_obj[1] |
| 1167 | if task.startswith('quilt-native'): |
| 1168 | quilt_native_deps += 1 |
| 1169 | elif task.startswith('quilt'): |
| 1170 | quilt_deps += 1 |
| 1171 | elif TYPE_RDEP == task_obj[0]: |
| 1172 | task = task_obj[1] |
| 1173 | if task.startswith('quilt-native'): |
| 1174 | quilt_native_rdeps += 1 |
| 1175 | elif task.startswith('quilt'): |
| 1176 | quilt_rdeps += 1 |
| 1177 | # Print results |
| 1178 | failed = False |
| 1179 | if 0 < len(dep.depends_model): |
| 1180 | print(f"Pass:Bitbake dependency count = {len(dep.depends_model)}") |
| 1181 | else: |
| 1182 | failed = True |
| 1183 | print(f"FAIL:Bitbake dependency count = 0") |
| 1184 | if quilt_native_deps: |
| 1185 | print(f"Pass:Quilt-native depends count = {quilt_native_deps}") |
| 1186 | else: |
| 1187 | failed = True |
| 1188 | print(f"FAIL:Quilt-native depends count = 0") |
| 1189 | if quilt_native_rdeps: |
| 1190 | print(f"Pass:Quilt-native rdepends count = {quilt_native_rdeps}") |
| 1191 | else: |
| 1192 | failed = True |
| 1193 | print(f"FAIL:Quilt-native rdepends count = 0") |
| 1194 | if quilt_deps: |
| 1195 | print(f"Pass:Quilt depends count = {quilt_deps}") |
| 1196 | else: |
| 1197 | failed = True |
| 1198 | print(f"FAIL:Quilt depends count = 0") |
| 1199 | if quilt_rdeps: |
| 1200 | print(f"Pass:Quilt rdepends count = {quilt_rdeps}") |
| 1201 | else: |
| 1202 | failed = True |
| 1203 | print(f"FAIL:Quilt rdepends count = 0") |
| 1204 | print("* UNIT TEST:STOP") |
| 1205 | return failed |
| 1206 | |
| 1207 | # Help method to dynamically test parent window too small |
| 1208 | def check_screen_size(dep, active_package): |
| 1209 | global screen_too_small |
| 1210 | rows, cols = screen.getmaxyx() |
| 1211 | if (rows >= SCREEN_ROW_MIN) and (cols >= SCREEN_COL_MIN): |
| 1212 | if screen_too_small: |
| 1213 | # Now big enough, remove error message and redraw screen |
| 1214 | dep.draw_frames() |
| 1215 | active_package.cursor_on(True) |
| 1216 | screen_too_small = False |
| 1217 | return True |
| 1218 | # Test on App init |
| 1219 | if not dep: |
| 1220 | # Do not start this app if screen not big enough |
| 1221 | curses.endwin() |
| 1222 | print("") |
| 1223 | print("ERROR(Taskexp_cli): Mininal screen size is %dx%d" % (SCREEN_COL_MIN,SCREEN_ROW_MIN)) |
| 1224 | print("Current screen is Cols=%s,Rows=%d" % (cols,rows)) |
| 1225 | return False |
| 1226 | # First time window too small |
| 1227 | if not screen_too_small: |
| 1228 | active_package.cursor_on(False) |
| 1229 | dep.screen.addstr(0,2,'[BIGGER WINDOW PLEASE]', curses.color_pair(CURSES_WARNING) | curses.A_BLINK) |
| 1230 | screen_too_small = True |
| 1231 | return False |
| 1232 | |
| 1233 | # Helper method to turn off curses mode |
| 1234 | def curses_off(screen): |
| 1235 | if not screen: return |
| 1236 | # Safe error exit |
| 1237 | screen.keypad(False) |
| 1238 | curses.echo() |
| 1239 | curses.curs_set(1) |
| 1240 | curses.endwin() |
| 1241 | |
| 1242 | if unit_test_results: |
| 1243 | print('\nUnit Test Results:') |
| 1244 | for line in unit_test_results: |
| 1245 | print(" %s" % line) |
| 1246 | |
| 1247 | # |
| 1248 | # Initialize the ncurse environment |
| 1249 | # |
| 1250 | |
| 1251 | screen = curses.initscr() |
| 1252 | try: |
| 1253 | if not check_screen_size(None, None): |
| 1254 | exit(1) |
| 1255 | try: |
| 1256 | curses.start_color() |
| 1257 | curses.use_default_colors(); |
| 1258 | curses.init_pair(0xFF, curses.COLOR_BLACK, curses.COLOR_WHITE); |
| 1259 | curses.init_pair(CURSES_NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK) |
| 1260 | curses.init_pair(CURSES_HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLUE) |
| 1261 | curses.init_pair(CURSES_WARNING, curses.COLOR_WHITE, curses.COLOR_RED) |
| 1262 | except: |
| 1263 | curses.endwin() |
| 1264 | print("") |
| 1265 | print("ERROR(Taskexp_cli): Requires 256 colors. Please use this or the equivalent:") |
| 1266 | print(" $ export TERM='xterm-256color'") |
| 1267 | exit(1) |
| 1268 | |
| 1269 | screen.keypad(True) |
| 1270 | curses.noecho() |
| 1271 | curses.curs_set(0) |
| 1272 | screen.refresh(); |
| 1273 | except Exception as e: |
| 1274 | # Safe error exit |
| 1275 | curses_off(screen) |
| 1276 | print("Exception : %s" % e) |
| 1277 | print("Exception in startup:\n %s" % traceback.format_exc()) |
| 1278 | exit(1) |
| 1279 | |
| 1280 | try: |
| 1281 | # |
| 1282 | # Instantiate the presentation layers |
| 1283 | # |
| 1284 | |
| 1285 | dep = DepExplorer(screen) |
| 1286 | |
| 1287 | # |
| 1288 | # Prepare bitbake |
| 1289 | # |
| 1290 | |
| 1291 | # Fetch bitbake dependecy data |
| 1292 | ret,cmdline = bitbake_load(server, eventHandler, params, dep, curses_off, screen) |
| 1293 | if ret: return ret |
| 1294 | |
| 1295 | # |
| 1296 | # Preset the views |
| 1297 | # |
| 1298 | |
| 1299 | # Cmdline example = ['generateDotGraph', ['acl', 'zlib'], 'build'] |
| 1300 | primary_packages = cmdline[1] |
| 1301 | dep.package_view.set_primary(primary_packages) |
| 1302 | dep.dep_view.set_primary(primary_packages) |
| 1303 | dep.reverse_view.set_primary(primary_packages) |
| 1304 | dep.help_box_view.set_primary(primary_packages) |
| 1305 | dep.help_bar_view.show_help(True) |
| 1306 | active_package = dep.package_view |
| 1307 | active_package.cursor_on(True) |
| 1308 | dep.select(primary_packages[0]+'.') |
| 1309 | if unit_test: |
| 1310 | alert('UNIT_TEST',screen) |
| 1311 | |
| 1312 | # Help method to start/stop the filter feature |
| 1313 | def filter_mode(new_filter_status): |
| 1314 | global is_filter |
| 1315 | if is_filter == new_filter_status: |
| 1316 | # Ignore no changes |
| 1317 | return |
| 1318 | if not new_filter_status: |
| 1319 | # Turn off |
| 1320 | curses.curs_set(0) |
| 1321 | #active_package.cursor_on(False) |
| 1322 | active_package = dep.package_view |
| 1323 | active_package.cursor_on(True) |
| 1324 | is_filter = False |
| 1325 | dep.help_bar_view.show_help(True) |
| 1326 | dep.filter_str = '' |
| 1327 | dep.select('') |
| 1328 | else: |
| 1329 | # Turn on |
| 1330 | curses.curs_set(1) |
| 1331 | dep.help_bar_view.show_help(False) |
| 1332 | dep.filter_view.clear() |
| 1333 | dep.filter_view.show(True) |
| 1334 | dep.filter_view.show_prompt() |
| 1335 | is_filter = True |
| 1336 | |
| 1337 | # |
| 1338 | # Main user loop |
| 1339 | # |
| 1340 | |
| 1341 | while not quit: |
| 1342 | if is_filter: |
| 1343 | dep.filter_view.show_prompt() |
| 1344 | if unit_test: |
| 1345 | c = unit_test_action(active_package) |
| 1346 | else: |
| 1347 | c = screen.getch() |
| 1348 | ch = chr(c) |
| 1349 | |
| 1350 | # Do not draw if window now too small |
| 1351 | if not check_screen_size(dep,active_package): |
| 1352 | continue |
| 1353 | |
| 1354 | if verbose: |
| 1355 | if c == CHAR_RETURN: |
| 1356 | screen.addstr(0, 4, "|%3d,CR |" % (c)) |
| 1357 | else: |
| 1358 | screen.addstr(0, 4, "|%3d,%3s|" % (c,chr(c))) |
| 1359 | |
| 1360 | # pre-map alternate filter close keys |
| 1361 | if is_filter and (c == CHAR_ESCAPE): |
| 1362 | # Alternate exit from filter |
| 1363 | ch = '/' |
| 1364 | c = ord(ch) |
| 1365 | |
| 1366 | # Filter and non-filter mode command keys |
| 1367 | # https://docs.python.org/3/library/curses.html |
| 1368 | if c in (curses.KEY_UP,CHAR_UP): |
| 1369 | active_package.line_up() |
| 1370 | if active_package == dep.package_view: |
| 1371 | dep.select('',only_update_dependents=True) |
| 1372 | elif c in (curses.KEY_DOWN,CHAR_DOWN): |
| 1373 | active_package.line_down() |
| 1374 | if active_package == dep.package_view: |
| 1375 | dep.select('',only_update_dependents=True) |
| 1376 | elif curses.KEY_PPAGE == c: |
| 1377 | active_package.page_up() |
| 1378 | if active_package == dep.package_view: |
| 1379 | dep.select('',only_update_dependents=True) |
| 1380 | elif curses.KEY_NPAGE == c: |
| 1381 | active_package.page_down() |
| 1382 | if active_package == dep.package_view: |
| 1383 | dep.select('',only_update_dependents=True) |
| 1384 | elif CHAR_TAB == c: |
| 1385 | # Tab between boxes |
| 1386 | active_package.cursor_on(False) |
| 1387 | if active_package == dep.package_view: |
| 1388 | active_package = dep.dep_view |
| 1389 | elif active_package == dep.dep_view: |
| 1390 | active_package = dep.reverse_view |
| 1391 | else: |
| 1392 | active_package = dep.package_view |
| 1393 | active_package.cursor_on(True) |
| 1394 | elif curses.KEY_BTAB == c: |
| 1395 | # Shift-Tab reverse between boxes |
| 1396 | active_package.cursor_on(False) |
| 1397 | if active_package == dep.package_view: |
| 1398 | active_package = dep.reverse_view |
| 1399 | elif active_package == dep.reverse_view: |
| 1400 | active_package = dep.dep_view |
| 1401 | else: |
| 1402 | active_package = dep.package_view |
| 1403 | active_package.cursor_on(True) |
| 1404 | elif (CHAR_RETURN == c): |
| 1405 | # CR to select |
| 1406 | selected = active_package.get_selected() |
| 1407 | if selected: |
| 1408 | active_package.cursor_on(False) |
| 1409 | active_package = dep.package_view |
| 1410 | filter_mode(False) |
| 1411 | dep.select(selected) |
| 1412 | else: |
| 1413 | filter_mode(False) |
| 1414 | dep.select(primary_packages[0]+'.') |
| 1415 | |
| 1416 | elif '/' == ch: # Enter/exit dep.filter_view |
| 1417 | if is_filter: |
| 1418 | filter_mode(False) |
| 1419 | else: |
| 1420 | filter_mode(True) |
| 1421 | elif is_filter: |
| 1422 | # If in filter mode, re-direct all these other keys to the filter box |
| 1423 | result = dep.filter_view.input(c,ch) |
| 1424 | dep.filter_str = dep.filter_view.filter_str |
| 1425 | dep.select('') |
| 1426 | |
| 1427 | # Non-filter mode command keys |
| 1428 | elif 'p' == ch: |
| 1429 | dep.print_deps(whole_group=False) |
| 1430 | elif 'P' == ch: |
| 1431 | dep.print_deps(whole_group=True) |
| 1432 | elif 'w' == ch: |
| 1433 | # Toggle the print model |
| 1434 | if print_model == PRINT_MODEL_1: |
| 1435 | print_model = PRINT_MODEL_2 |
| 1436 | else: |
| 1437 | print_model = PRINT_MODEL_1 |
| 1438 | elif 's' == ch: |
| 1439 | # Toggle the sort model |
| 1440 | if sort_model == SORT_DEPS: |
| 1441 | sort_model = SORT_ALPHA |
| 1442 | elif sort_model == SORT_ALPHA: |
| 1443 | if SORT_BITBAKE_ENABLE: |
| 1444 | sort_model = TASK_SORT_BITBAKE |
| 1445 | else: |
| 1446 | sort_model = SORT_DEPS |
| 1447 | else: |
| 1448 | sort_model = SORT_DEPS |
| 1449 | active_package.cursor_on(False) |
| 1450 | current_task = active_package.get_selected() |
| 1451 | dep.package_view.sort() |
| 1452 | dep.dep_view.sort() |
| 1453 | dep.reverse_view.sort() |
| 1454 | active_package = dep.package_view |
| 1455 | active_package.cursor_on(True) |
| 1456 | dep.select(current_task) |
| 1457 | # Announce the new sort model |
| 1458 | alert("SORT=%s" % ("ALPHA" if (sort_model == SORT_ALPHA) else "DEPS"),screen) |
| 1459 | alert('',screen) |
| 1460 | |
| 1461 | elif 'q' == ch: |
| 1462 | quit = True |
| 1463 | elif ch in ('h','?'): |
| 1464 | dep.help_box_view.show_help(True) |
| 1465 | dep.select(active_package.get_selected()) |
| 1466 | |
| 1467 | # |
| 1468 | # Debugging commands |
| 1469 | # |
| 1470 | |
| 1471 | elif 'V' == ch: |
| 1472 | verbose = not verbose |
| 1473 | alert('Verbose=%s' % str(verbose),screen) |
| 1474 | alert('',screen) |
| 1475 | elif 'R' == ch: |
| 1476 | screen.refresh() |
| 1477 | elif 'B' == ch: |
| 1478 | # Progress bar unit test |
| 1479 | dep.progress_view.progress('Test',0,40) |
| 1480 | curses.napms(1000) |
| 1481 | dep.progress_view.progress('',10,40) |
| 1482 | curses.napms(1000) |
| 1483 | dep.progress_view.progress('',20,40) |
| 1484 | curses.napms(1000) |
| 1485 | dep.progress_view.progress('',30,40) |
| 1486 | curses.napms(1000) |
| 1487 | dep.progress_view.progress('',40,40) |
| 1488 | curses.napms(1000) |
| 1489 | dep.progress_view.clear() |
| 1490 | dep.help_bar_view.show_help(True) |
| 1491 | elif 'Q' == ch: |
| 1492 | # Simulated error |
| 1493 | curses_off(screen) |
| 1494 | print('ERROR: simulated error exit') |
| 1495 | return 1 |
| 1496 | |
| 1497 | # Safe exit |
| 1498 | curses_off(screen) |
| 1499 | except Exception as e: |
| 1500 | # Safe exit on error |
| 1501 | curses_off(screen) |
| 1502 | print("Exception : %s" % e) |
| 1503 | print("Exception in startup:\n %s" % traceback.format_exc()) |
| 1504 | |
| 1505 | # Reminder to pick up your printed results |
| 1506 | if is_printed: |
| 1507 | print("") |
| 1508 | print("You have output ready!") |
| 1509 | print(" * Your printed dependency file is: %s" % print_file_name) |
| 1510 | print(" * Your previous results saved in: %s" % print_file_backup_name) |
| 1511 | print("") |