summaryrefslogtreecommitdiffstats
path: root/bitbake/lib/bb/ui
diff options
context:
space:
mode:
authorDavid Reyna <David.Reyna@windriver.com>2024-02-18 02:41:55 -0800
committerRichard Purdie <richard.purdie@linuxfoundation.org>2024-02-18 17:38:47 +0000
commitf62e4d81ec584c4fb2f052755ac90d460d32bca2 (patch)
treebb2703ef9039f956041635c378be4a42c540e005 /bitbake/lib/bb/ui
parent3c08950557aaaa39302e07cf6a99ed950a0b2b43 (diff)
downloadpoky-f62e4d81ec584c4fb2f052755ac90d460d32bca2.tar.gz
bitbake: taskexp_ncurses: ncurses version of taskexp.py
* Create an ncurses version of the GTK app "taskexp.py". * Add these additional features: - Sort tasks in recipes by their dependency order - Print individual and/or recipe-wide dependencies to a file - Add a wild card filter - Show the target recipes on BOLD * Provide a GUI self test * Provide a non-ncurses self test for ptest (Bitbake rev: f49bec66ad51c8cddeceafbbe2445c46e396ee8b) Signed-off-by: David Reyna <David.Reyna@windriver.com> Signed-off-by: Richard Purdie <richard.purdie@linuxfoundation.org>
Diffstat (limited to 'bitbake/lib/bb/ui')
-rwxr-xr-xbitbake/lib/bb/ui/taskexp_ncurses.py1511
1 files changed, 1511 insertions, 0 deletions
diff --git a/bitbake/lib/bb/ui/taskexp_ncurses.py b/bitbake/lib/bb/ui/taskexp_ncurses.py
new file mode 100755
index 0000000000..dd91d26bc3
--- /dev/null
+++ b/bitbake/lib/bb/ui/taskexp_ncurses.py
@@ -0,0 +1,1511 @@
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.py acl zlib
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("")