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