runqueue.py: Change failed tasks handling so all failed tasks are reported, not just...
[vuplus_bitbake] / lib / bb / shell.py
1 #!/usr/bin/env python
2 # ex:ts=4:sw=4:sts=4:et
3 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4 ##########################################################################
5 #
6 # Copyright (C) 2005-2006 Michael 'Mickey' Lauer <mickey@Vanille.de>
7 # Copyright (C) 2005-2006 Vanille Media
8 #
9 # This program is free software; you can redistribute it and/or modify it under
10 # the terms of the GNU General Public License as published by the Free Software
11 # Foundation; version 2 of the License.
12 #
13 # This program is distributed in the hope that it will be useful, but WITHOUT
14 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
15 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 #
17 # You should have received a copy of the GNU General Public License along with
18 # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
19 # Place, Suite 330, Boston, MA 02111-1307 USA.
20 #
21 ##########################################################################
22 #
23 # Thanks to:
24 # * Holger Freyther <zecke@handhelds.org>
25 # * Justin Patrin <papercrane@reversefold.com>
26 #
27 ##########################################################################
28
29 """
30 BitBake Shell
31
32 IDEAS:
33     * list defined tasks per package
34     * list classes
35     * toggle force
36     * command to reparse just one (or more) bbfile(s)
37     * automatic check if reparsing is necessary (inotify?)
38     * frontend for bb file manipulation
39     * more shell-like features:
40         - output control, i.e. pipe output into grep, sort, etc.
41         - job control, i.e. bring running commands into background and foreground
42     * start parsing in background right after startup
43     * ncurses interface
44
45 PROBLEMS:
46     * force doesn't always work
47     * readline completion for commands with more than one parameters
48
49 """
50
51 ##########################################################################
52 # Import and setup global variables
53 ##########################################################################
54
55 try:
56     set
57 except NameError:
58     from sets import Set as set
59 import sys, os, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
60 from bb import data, parse, build, fatal, cache, taskdata, runqueue, providers as Providers
61
62 __version__ = "0.5.3.1"
63 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
64 Type 'help' for more information, press CTRL-D to exit.""" % __version__
65
66 cmds = {}
67 leave_mainloop = False
68 last_exception = None
69 cooker = None
70 parsed = False
71 initdata = None
72 debug = os.environ.get( "BBSHELL_DEBUG", "" )
73
74 ##########################################################################
75 # Class BitBakeShellCommands
76 ##########################################################################
77
78 class BitBakeShellCommands:
79     """This class contains the valid commands for the shell"""
80
81     def __init__( self, shell ):
82         """Register all the commands"""
83         self._shell = shell
84         for attr in BitBakeShellCommands.__dict__:
85             if not attr.startswith( "_" ):
86                 if attr.endswith( "_" ):
87                     command = attr[:-1].lower()
88                 else:
89                     command = attr[:].lower()
90                 method = getattr( BitBakeShellCommands, attr )
91                 debugOut( "registering command '%s'" % command )
92                 # scan number of arguments
93                 usage = getattr( method, "usage", "" )
94                 if usage != "<...>":
95                     numArgs = len( usage.split() )
96                 else:
97                     numArgs = -1
98                 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
99
100     def _checkParsed( self ):
101         if not parsed:
102             print "SHELL: This command needs to parse bbfiles..."
103             self.parse( None )
104
105     def _findProvider( self, item ):
106         self._checkParsed()
107         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
108         if not preferred: preferred = item
109         try:
110             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status, cooker.build_cache_fail)
111         except KeyError:
112             if item in cooker.status.providers:
113                 pf = cooker.status.providers[item][0]
114             else:
115                 pf = None
116         return pf
117
118     def alias( self, params ):
119         """Register a new name for a command"""
120         new, old = params
121         if not old in cmds:
122             print "ERROR: Command '%s' not known" % old
123         else:
124             cmds[new] = cmds[old]
125             print "OK"
126     alias.usage = "<alias> <command>"
127
128     def buffer( self, params ):
129         """Dump specified output buffer"""
130         index = params[0]
131         print self._shell.myout.buffer( int( index ) )
132     buffer.usage = "<index>"
133
134     def buffers( self, params ):
135         """Show the available output buffers"""
136         commands = self._shell.myout.bufferedCommands()
137         if not commands:
138             print "SHELL: No buffered commands available yet. Start doing something."
139         else:
140             print "="*35, "Available Output Buffers", "="*27
141             for index, cmd in enumerate( commands ):
142                 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
143             print "="*88
144
145     def build( self, params, cmd = "build" ):
146         """Build a providee"""
147         globexpr = params[0]
148         self._checkParsed()
149         names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
150         if len( names ) == 0: names = [ globexpr ]
151         print "SHELL: Building %s" % ' '.join( names )
152
153         oldcmd = cooker.configuration.cmd
154         cooker.configuration.cmd = cmd
155         cooker.build_cache = []
156         cooker.build_cache_fail = []
157
158         td = taskdata.TaskData(cooker.configuration.abort)
159
160         try:
161             tasks = []
162             for name in names:
163                 td.add_provider(cooker.configuration.data, cooker.status, name)
164                 providers = td.get_provider(name)
165
166                 if len(providers) == 0:
167                     raise Providers.NoProvider
168
169                 tasks.append([name, "do_%s" % cooker.configuration.cmd])
170
171             td.add_unresolved(cooker.configuration.data, cooker.status)
172             
173             rq = runqueue.RunQueue()
174             rq.prepare_runqueue(cooker.configuration.data, cooker.status, td, tasks)
175             rq.execute_runqueue(cooker, cooker.configuration.data, cooker.status, td, tasks)
176
177         except Providers.NoProvider:
178             print "ERROR: No Provider"
179             global last_exception
180             last_exception = Providers.NoProvider
181
182         except runqueue.TaskFailure, fnids:
183             for fnid in fnids:
184                 print "ERROR: '%s' failed" % td.fn_index[fnid])
185             global last_exception
186             last_exception = runqueue.TaskFailure
187
188         except build.EventException, e:
189             print "ERROR: Couldn't build '%s'" % names
190             global last_exception
191             last_exception = e
192
193         cooker.configuration.cmd = oldcmd
194
195     build.usage = "<providee>"
196
197     def clean( self, params ):
198         """Clean a providee"""
199         self.build( params, "clean" )
200     clean.usage = "<providee>"
201
202     def compile( self, params ):
203         """Execute 'compile' on a providee"""
204         self.build( params, "compile" )
205     compile.usage = "<providee>"
206
207     def configure( self, params ):
208         """Execute 'configure' on a providee"""
209         self.build( params, "configure" )
210     configure.usage = "<providee>"
211
212     def edit( self, params ):
213         """Call $EDITOR on a providee"""
214         name = params[0]
215         bbfile = self._findProvider( name )
216         if bbfile is not None:
217             os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
218         else:
219             print "ERROR: Nothing provides '%s'" % name
220     edit.usage = "<providee>"
221
222     def environment( self, params ):
223         """Dump out the outer BitBake environment (see bbread)"""
224         data.emit_env(sys.__stdout__, cooker.configuration.data, True)
225
226     def exit_( self, params ):
227         """Leave the BitBake Shell"""
228         debugOut( "setting leave_mainloop to true" )
229         global leave_mainloop
230         leave_mainloop = True
231
232     def fetch( self, params ):
233         """Fetch a providee"""
234         self.build( params, "fetch" )
235     fetch.usage = "<providee>"
236
237     def fileBuild( self, params, cmd = "build" ):
238         """Parse and build a .bb file"""
239         name = params[0]
240         bf = completeFilePath( name )
241         print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
242
243         oldcmd = cooker.configuration.cmd
244         cooker.configuration.cmd = cmd
245         cooker.build_cache = []
246         cooker.build_cache_fail = []
247
248         thisdata = copy.deepcopy( initdata )
249         # Caution: parse.handle modifies thisdata, hence it would
250         # lead to pollution cooker.configuration.data, which is
251         # why we use it on a safe copy we obtained from cooker right after
252         # parsing the initial *.conf files
253         try:
254             bbfile_data = parse.handle( bf, thisdata )
255         except parse.ParseError:
256             print "ERROR: Unable to open or parse '%s'" % bf
257         else:
258             item = data.getVar('PN', bbfile_data, 1)
259             data.setVar( "_task_cache", [], bbfile_data ) # force
260             try:
261                 cooker.tryBuildPackage( os.path.abspath( bf ), item, cmd, bbfile_data, True )
262             except build.EventException, e:
263                 print "ERROR: Couldn't build '%s'" % name
264                 global last_exception
265                 last_exception = e
266
267         cooker.configuration.cmd = oldcmd
268     fileBuild.usage = "<bbfile>"
269
270     def fileClean( self, params ):
271         """Clean a .bb file"""
272         self.fileBuild( params, "clean" )
273     fileClean.usage = "<bbfile>"
274
275     def fileEdit( self, params ):
276         """Call $EDITOR on a .bb file"""
277         name = params[0]
278         os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
279     fileEdit.usage = "<bbfile>"
280
281     def fileRebuild( self, params ):
282         """Rebuild (clean & build) a .bb file"""
283         self.fileBuild( params, "rebuild" )
284     fileRebuild.usage = "<bbfile>"
285
286     def fileReparse( self, params ):
287         """(re)Parse a bb file"""
288         bbfile = params[0]
289         print "SHELL: Parsing '%s'" % bbfile
290         parse.update_mtime( bbfile )
291         cooker.bb_cache.cacheValidUpdate(bbfile)
292         fromCache = cooker.bb_cache.loadData(bbfile, cooker.configuration.data)
293         cooker.bb_cache.sync()
294         if False: #fromCache:
295             print "SHELL: File has not been updated, not reparsing"
296         else:
297             print "SHELL: Parsed"
298     fileReparse.usage = "<bbfile>"
299
300     def abort( self, params ):
301         """Toggle abort task execution flag (see bitbake -k)"""
302         cooker.configuration.abort = not cooker.configuration.abort
303         print "SHELL: Abort Flag is now '%s'" % repr( cooker.configuration.abort )
304
305     def force( self, params ):
306         """Toggle force task execution flag (see bitbake -f)"""
307         cooker.configuration.force = not cooker.configuration.force
308         print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
309
310     def help( self, params ):
311         """Show a comprehensive list of commands and their purpose"""
312         print "="*30, "Available Commands", "="*30
313         allcmds = cmds.keys()
314         allcmds.sort()
315         for cmd in allcmds:
316             function,numparams,usage,helptext = cmds[cmd]
317             print "| %s | %s" % (usage.ljust(30), helptext)
318         print "="*78
319
320     def lastError( self, params ):
321         """Show the reason or log that was produced by the last BitBake event exception"""
322         if last_exception is None:
323             print "SHELL: No Errors yet (Phew)..."
324         else:
325             reason, event = last_exception.args
326             print "SHELL: Reason for the last error: '%s'" % reason
327             if ':' in reason:
328                 msg, filename = reason.split( ':' )
329                 filename = filename.strip()
330                 print "SHELL: Dumping log file for last error:"
331                 try:
332                     print open( filename ).read()
333                 except IOError:
334                     print "ERROR: Couldn't open '%s'" % filename
335
336     def match( self, params ):
337         """Dump all files or providers matching a glob expression"""
338         what, globexpr = params
339         if what == "files":
340             self._checkParsed()
341             for key in globfilter( cooker.status.pkg_fn.keys(), globexpr ): print key
342         elif what == "providers":
343             self._checkParsed()
344             for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
345         else:
346             print "Usage: match %s" % self.print_.usage
347     match.usage = "<files|providers> <glob>"
348
349     def new( self, params ):
350         """Create a new .bb file and open the editor"""
351         dirname, filename = params
352         packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
353         fulldirname = "%s/%s" % ( packages, dirname )
354
355         if not os.path.exists( fulldirname ):
356             print "SHELL: Creating '%s'" % fulldirname
357             os.mkdir( fulldirname )
358         if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
359             if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
360                 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
361                 return False
362             print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
363             newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
364             print >>newpackage,"""DESCRIPTION = ""
365 SECTION = ""
366 AUTHOR = ""
367 HOMEPAGE = ""
368 MAINTAINER = ""
369 LICENSE = "GPL"
370 PR = "r0"
371
372 SRC_URI = ""
373
374 #inherit base
375
376 #do_configure() {
377 #
378 #}
379
380 #do_compile() {
381 #
382 #}
383
384 #do_stage() {
385 #
386 #}
387
388 #do_install() {
389 #
390 #}
391 """
392             newpackage.close()
393             os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
394     new.usage = "<directory> <filename>"
395
396     def pasteBin( self, params ):
397         """Send a command + output buffer to the pastebin at http://rafb.net/paste"""
398         index = params[0]
399         contents = self._shell.myout.buffer( int( index ) )
400         sendToPastebin( "output of " + params[0], contents )
401     pasteBin.usage = "<index>"
402
403     def pasteLog( self, params ):
404         """Send the last event exception error log (if there is one) to http://rafb.net/paste"""
405         if last_exception is None:
406             print "SHELL: No Errors yet (Phew)..."
407         else:
408             reason, event = last_exception.args
409             print "SHELL: Reason for the last error: '%s'" % reason
410             if ':' in reason:
411                 msg, filename = reason.split( ':' )
412                 filename = filename.strip()
413                 print "SHELL: Pasting log file to pastebin..."
414
415                 file = open( filename ).read()
416                 sendToPastebin( "contents of " + filename, file )
417
418     def patch( self, params ):
419         """Execute 'patch' command on a providee"""
420         self.build( params, "patch" )
421     patch.usage = "<providee>"
422
423     def parse( self, params ):
424         """(Re-)parse .bb files and calculate the dependency graph"""
425         cooker.status = cache.CacheData()
426         ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
427         cooker.status.ignored_dependencies = set( ignore.split() )
428         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
429
430         (filelist, masked) = cooker.collect_bbfiles()
431         cooker.parse_bbfiles(filelist, masked, cooker.myProgressCallback)
432         cooker.buildDepgraph()
433         global parsed
434         parsed = True
435         print
436
437     def reparse( self, params ):
438         """(re)Parse a providee's bb file"""
439         bbfile = self._findProvider( params[0] )
440         if bbfile is not None:
441             print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
442             self.fileReparse( [ bbfile ] )
443         else:
444             print "ERROR: Nothing provides '%s'" % params[0]
445     reparse.usage = "<providee>"
446
447     def getvar( self, params ):
448         """Dump the contents of an outer BitBake environment variable"""
449         var = params[0]
450         value = data.getVar( var, cooker.configuration.data, 1 )
451         print value
452     getvar.usage = "<variable>"
453
454     def peek( self, params ):
455         """Dump contents of variable defined in providee's metadata"""
456         name, var = params
457         bbfile = self._findProvider( name )
458         if bbfile is not None:
459             the_data = cooker.bb_cache.loadDataFull(bbfile, cooker.configuration.data)
460             value = the_data.getVar( var, 1 )
461             print value
462         else:
463             print "ERROR: Nothing provides '%s'" % name
464     peek.usage = "<providee> <variable>"
465
466     def poke( self, params ):
467         """Set contents of variable defined in providee's metadata"""
468         name, var, value = params
469         bbfile = self._findProvider( name )
470         if bbfile is not None:
471             print "ERROR: Sorry, this functionality is currently broken"
472             #d = cooker.pkgdata[bbfile]
473             #data.setVar( var, value, d )
474
475             # mark the change semi persistant
476             #cooker.pkgdata.setDirty(bbfile, d)
477             #print "OK"
478         else:
479             print "ERROR: Nothing provides '%s'" % name
480     poke.usage = "<providee> <variable> <value>"
481
482     def print_( self, params ):
483         """Dump all files or providers"""
484         what = params[0]
485         if what == "files":
486             self._checkParsed()
487             for key in cooker.status.pkg_fn.keys(): print key
488         elif what == "providers":
489             self._checkParsed()
490             for key in cooker.status.providers.keys(): print key
491         else:
492             print "Usage: print %s" % self.print_.usage
493     print_.usage = "<files|providers>"
494
495     def python( self, params ):
496         """Enter the expert mode - an interactive BitBake Python Interpreter"""
497         sys.ps1 = "EXPERT BB>>> "
498         sys.ps2 = "EXPERT BB... "
499         import code
500         interpreter = code.InteractiveConsole( dict( globals() ) )
501         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
502
503     def showdata( self, params ):
504         """Execute 'showdata' on a providee"""
505         self.build( params, "showdata" )
506     showdata.usage = "<providee>"
507
508     def setVar( self, params ):
509         """Set an outer BitBake environment variable"""
510         var, value = params
511         data.setVar( var, value, cooker.configuration.data )
512         print "OK"
513     setVar.usage = "<variable> <value>"
514
515     def rebuild( self, params ):
516         """Clean and rebuild a .bb file or a providee"""
517         self.build( params, "clean" )
518         self.build( params, "build" )
519     rebuild.usage = "<providee>"
520
521     def shell( self, params ):
522         """Execute a shell command and dump the output"""
523         if params != "":
524             print commands.getoutput( " ".join( params ) )
525     shell.usage = "<...>"
526
527     def stage( self, params ):
528         """Execute 'stage' on a providee"""
529         self.build( params, "stage" )
530     stage.usage = "<providee>"
531
532     def status( self, params ):
533         """<just for testing>"""
534         print "-" * 78
535         print "build cache = '%s'" % cooker.build_cache
536         print "build cache fail = '%s'" % cooker.build_cache_fail
537         print "building list = '%s'" % cooker.building_list
538         print "build path = '%s'" % cooker.build_path
539         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
540         print "build stats = '%s'" % cooker.stats
541         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
542         print "memory output contents = '%s'" % self._shell.myout._buffer
543
544     def test( self, params ):
545         """<just for testing>"""
546         print "testCommand called with '%s'" % params
547
548     def unpack( self, params ):
549         """Execute 'unpack' on a providee"""
550         self.build( params, "unpack" )
551     unpack.usage = "<providee>"
552
553     def which( self, params ):
554         """Computes the providers for a given providee"""
555         item = params[0]
556
557         self._checkParsed()
558
559         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
560         if not preferred: preferred = item
561
562         try:
563             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status, 
564 cooker.build_cache_fail)
565         except KeyError:
566             lv, lf, pv, pf = (None,)*4
567
568         try:
569             providers = cooker.status.providers[item]
570         except KeyError:
571             print "SHELL: ERROR: Nothing provides", preferred
572         else:
573             for provider in providers:
574                 if provider == pf: provider = " (***) %s" % provider
575                 else:              provider = "       %s" % provider
576                 print provider
577     which.usage = "<providee>"
578
579 ##########################################################################
580 # Common helper functions
581 ##########################################################################
582
583 def completeFilePath( bbfile ):
584     """Get the complete bbfile path"""
585     if not cooker.status.pkg_fn: return bbfile
586     for key in cooker.status.pkg_fn.keys():
587         if key.endswith( bbfile ):
588             return key
589     return bbfile
590
591 def sendToPastebin( desc, content ):
592     """Send content to http://oe.pastebin.com"""
593     mydata = {}
594     mydata["lang"] = "Plain Text"
595     mydata["desc"] = desc
596     mydata["cvt_tabs"] = "No"
597     mydata["nick"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
598     mydata["text"] = content
599     params = urllib.urlencode( mydata )
600     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
601
602     host = "rafb.net"
603     conn = httplib.HTTPConnection( "%s:80" % host )
604     conn.request("POST", "/paste/paste.php", params, headers )
605
606     response = conn.getresponse()
607     conn.close()
608
609     if response.status == 302:
610         location = response.getheader( "location" ) or "unknown"
611         print "SHELL: Pasted to http://%s%s" % ( host, location )
612     else:
613         print "ERROR: %s %s" % ( response.status, response.reason )
614
615 def completer( text, state ):
616     """Return a possible readline completion"""
617     debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
618
619     if state == 0:
620         line = readline.get_line_buffer()
621         if " " in line:
622             line = line.split()
623             # we are in second (or more) argument
624             if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
625                 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
626                 if u == "<variable>":
627                     allmatches = cooker.configuration.data.keys()
628                 elif u == "<bbfile>":
629                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
630                     else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
631                 elif u == "<providee>":
632                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
633                     else: allmatches = cooker.status.providers.iterkeys()
634                 else: allmatches = [ "(No tab completion available for this command)" ]
635             else: allmatches = [ "(No tab completion available for this command)" ]
636         else:
637             # we are in first argument
638             allmatches = cmds.iterkeys()
639
640         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
641         #print "completer.matches = '%s'" % completer.matches
642     if len( completer.matches ) > state:
643         return completer.matches[state]
644     else:
645         return None
646
647 def debugOut( text ):
648     if debug:
649         sys.stderr.write( "( %s )\n" % text )
650
651 def columnize( alist, width = 80 ):
652     """
653     A word-wrap function that preserves existing line breaks
654     and most spaces in the text. Expects that existing line
655     breaks are posix newlines (\n).
656     """
657     return reduce(lambda line, word, width=width: '%s%s%s' %
658                   (line,
659                    ' \n'[(len(line[line.rfind('\n')+1:])
660                          + len(word.split('\n',1)[0]
661                               ) >= width)],
662                    word),
663                   alist
664                  )
665
666 def globfilter( names, pattern ):
667     return fnmatch.filter( names, pattern )
668
669 ##########################################################################
670 # Class MemoryOutput
671 ##########################################################################
672
673 class MemoryOutput:
674     """File-like output class buffering the output of the last 10 commands"""
675     def __init__( self, delegate ):
676         self.delegate = delegate
677         self._buffer = []
678         self.text = []
679         self._command = None
680
681     def startCommand( self, command ):
682         self._command = command
683         self.text = []
684     def endCommand( self ):
685         if self._command is not None:
686             if len( self._buffer ) == 10: del self._buffer[0]
687             self._buffer.append( ( self._command, self.text ) )
688     def removeLast( self ):
689         if self._buffer:
690             del self._buffer[ len( self._buffer ) - 1 ]
691         self.text = []
692         self._command = None
693     def lastBuffer( self ):
694         if self._buffer:
695             return self._buffer[ len( self._buffer ) -1 ][1]
696     def bufferedCommands( self ):
697         return [ cmd for cmd, output in self._buffer ]
698     def buffer( self, i ):
699         if i < len( self._buffer ):
700             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
701         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
702     def write( self, text ):
703         if self._command is not None and text != "BB>> ": self.text.append( text )
704         if self.delegate is not None: self.delegate.write( text )
705     def flush( self ):
706         return self.delegate.flush()
707     def fileno( self ):
708         return self.delegate.fileno()
709     def isatty( self ):
710         return self.delegate.isatty()
711
712 ##########################################################################
713 # Class BitBakeShell
714 ##########################################################################
715
716 class BitBakeShell:
717
718     def __init__( self ):
719         """Register commands and set up readline"""
720         self.commandQ = Queue.Queue()
721         self.commands = BitBakeShellCommands( self )
722         self.myout = MemoryOutput( sys.stdout )
723         self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
724         self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
725
726         readline.set_completer( completer )
727         readline.set_completer_delims( " " )
728         readline.parse_and_bind("tab: complete")
729
730         try:
731             readline.read_history_file( self.historyfilename )
732         except IOError:
733             pass  # It doesn't exist yet.
734
735         print __credits__
736
737         # save initial cooker configuration (will be reused in file*** commands)
738         global initdata
739         initdata = copy.deepcopy( cooker.configuration.data )
740
741     def cleanup( self ):
742         """Write readline history and clean up resources"""
743         debugOut( "writing command history" )
744         try:
745             readline.write_history_file( self.historyfilename )
746         except:
747             print "SHELL: Unable to save command history"
748
749     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
750         """Register a command"""
751         if usage == "": usage = command
752         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
753         cmds[command] = ( function, numparams, usage, helptext )
754
755     def processCommand( self, command, params ):
756         """Process a command. Check number of params and print a usage string, if appropriate"""
757         debugOut( "processing command '%s'..." % command )
758         try:
759             function, numparams, usage, helptext = cmds[command]
760         except KeyError:
761             print "SHELL: ERROR: '%s' command is not a valid command." % command
762             self.myout.removeLast()
763         else:
764             if (numparams != -1) and (not len( params ) == numparams):
765                 print "Usage: '%s'" % usage
766                 return
767
768             result = function( self.commands, params )
769             debugOut( "result was '%s'" % result )
770
771     def processStartupFile( self ):
772         """Read and execute all commands found in $HOME/.bbsh_startup"""
773         if os.path.exists( self.startupfilename ):
774             startupfile = open( self.startupfilename, "r" )
775             for cmdline in startupfile:
776                 debugOut( "processing startup line '%s'" % cmdline )
777                 if not cmdline:
778                     continue
779                 if "|" in cmdline:
780                     print "ERROR: '|' in startup file is not allowed. Ignoring line"
781                     continue
782                 self.commandQ.put( cmdline.strip() )
783
784     def main( self ):
785         """The main command loop"""
786         while not leave_mainloop:
787             try:
788                 if self.commandQ.empty():
789                     sys.stdout = self.myout.delegate
790                     cmdline = raw_input( "BB>> " )
791                     sys.stdout = self.myout
792                 else:
793                     cmdline = self.commandQ.get()
794                 if cmdline:
795                     allCommands = cmdline.split( ';' )
796                     for command in allCommands:
797                         pipecmd = None
798                         #
799                         # special case for expert mode
800                         if command == 'python':
801                             sys.stdout = self.myout.delegate
802                             self.processCommand( command, "" )
803                             sys.stdout = self.myout
804                         else:
805                             self.myout.startCommand( command )
806                             if '|' in command: # disable output
807                                 command, pipecmd = command.split( '|' )
808                                 delegate = self.myout.delegate
809                                 self.myout.delegate = None
810                             tokens = shlex.split( command, True )
811                             self.processCommand( tokens[0], tokens[1:] or "" )
812                             self.myout.endCommand()
813                             if pipecmd is not None: # restore output
814                                 self.myout.delegate = delegate
815
816                                 pipe = popen2.Popen4( pipecmd )
817                                 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
818                                 pipe.tochild.close()
819                                 sys.stdout.write( pipe.fromchild.read() )
820                         #
821             except EOFError:
822                 print
823                 return
824             except KeyboardInterrupt:
825                 print
826
827 ##########################################################################
828 # Start function - called from the BitBake command line utility
829 ##########################################################################
830
831 def start( aCooker ):
832     global cooker
833     cooker = aCooker
834     bbshell = BitBakeShell()
835     bbshell.processStartupFile()
836     bbshell.main()
837     bbshell.cleanup()
838
839 if __name__ == "__main__":
840     print "SHELL: Sorry, this program should only be called by BitBake."