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: 15# $ bitbake -g -u taskexp_ncurses zlib acl 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 57import sys 58import traceback 59import curses 60import re 61import time 62 63# Bitbake server support 64import threading 65from xmlrpc import client 66import bb 67import bb.event 68 69# Dependency indexes (depends_model) 70(TYPE_DEP, TYPE_RDEP) = (0, 1) 71DEPENDS_TYPE = 0 72DEPENDS_TASK = 1 73DEPENDS_DEPS = 2 74# Task indexes (task_list) 75TASK_NAME = 0 76TASK_PRIMARY = 1 77TASK_SORT_ALPHA = 2 78TASK_SORT_DEPS = 3 79TASK_SORT_BITBAKE = 4 80# Sort options (default is SORT_DEPS) 81SORT_ALPHA = 0 82SORT_DEPS = 1 83SORT_BITBAKE_ENABLE = False # NOTE: future sort 84SORT_BITBAKE = 2 85sort_model = SORT_DEPS 86# Print options 87PRINT_MODEL_1 = 0 88PRINT_MODEL_2 = 1 89print_model = PRINT_MODEL_2 90print_file_name = "taskdep_print.log" 91print_file_backup_name = "taskdep_print_backup.log" 92is_printed = False 93is_filter = False 94 95# Standard (and backup) key mappings 96CHAR_NUL = 0 # Used as self-test nop char 97CHAR_BS_H = 8 # Alternate backspace key 98CHAR_TAB = 9 99CHAR_RETURN = 10 100CHAR_ESCAPE = 27 101CHAR_UP = ord('{') # Used as self-test ASCII char 102CHAR_DOWN = ord('}') # Used as self-test ASCII char 103 104# Color_pair IDs 105CURSES_NORMAL = 0 106CURSES_HIGHLIGHT = 1 107CURSES_WARNING = 2 108 109 110################################################# 111### Debugging support 112### 113 114verbose = False 115 116# Debug: message display slow-step through display update issues 117def 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 131def 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 146unit_test = os.environ.get('TASK_EXP_UNIT_TEST') 147unit_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] 170unit_test_idx=0 171unit_test_command_chars='' 172unit_test_results=[] 173def 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) 212unit_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 225do_line_art = True 226 227# ASCII render set option 228CHAR_HBAR = '-' 229CHAR_VBAR = '|' 230CHAR_UL_CORNER = '/' 231CHAR_UR_CORNER = '\\' 232CHAR_LL_CORNER = '\\' 233CHAR_LR_CORNER = '/' 234 235# Box frame drawing with line-art 236def 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 269def 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 287class 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 504class 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 517class 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 569class 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 590class 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 637class 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 673class 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 678class 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 687class 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 945def 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 1133SCREEN_COL_MIN = 83 1134SCREEN_ROW_MIN = 26 1135 1136def 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("") 1512