taskdata.py: Improve abort flag handling, fixing several bugs
[bitbake.git] / 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, (fnid, fn, taskname):
183             print "ERROR: '%s, %s' failed" % (fn, taskname)
184             global last_exception
185             last_exception = runqueue.TaskFailure
186
187         except build.EventException, e:
188             print "ERROR: Couldn't build '%s'" % names
189             global last_exception
190             last_exception = e
191
192         cooker.configuration.cmd = oldcmd
193
194     build.usage = "<providee>"
195
196     def clean( self, params ):
197         """Clean a providee"""
198         self.build( params, "clean" )
199     clean.usage = "<providee>"
200
201     def compile( self, params ):
202         """Execute 'compile' on a providee"""
203         self.build( params, "compile" )
204     compile.usage = "<providee>"
205
206     def configure( self, params ):
207         """Execute 'configure' on a providee"""
208         self.build( params, "configure" )
209     configure.usage = "<providee>"
210
211     def edit( self, params ):
212         """Call $EDITOR on a providee"""
213         name = params[0]
214         bbfile = self._findProvider( name )
215         if bbfile is not None:
216             os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
217         else:
218             print "ERROR: Nothing provides '%s'" % name
219     edit.usage = "<providee>"
220
221     def environment( self, params ):
222         """Dump out the outer BitBake environment (see bbread)"""
223         data.emit_env(sys.__stdout__, cooker.configuration.data, True)
224
225     def exit_( self, params ):
226         """Leave the BitBake Shell"""
227         debugOut( "setting leave_mainloop to true" )
228         global leave_mainloop
229         leave_mainloop = True
230
231     def fetch( self, params ):
232         """Fetch a providee"""
233         self.build( params, "fetch" )
234     fetch.usage = "<providee>"
235
236     def fileBuild( self, params, cmd = "build" ):
237         """Parse and build a .bb file"""
238         name = params[0]
239         bf = completeFilePath( name )
240         print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
241
242         oldcmd = cooker.configuration.cmd
243         cooker.configuration.cmd = cmd
244         cooker.build_cache = []
245         cooker.build_cache_fail = []
246
247         thisdata = copy.deepcopy( initdata )
248         # Caution: parse.handle modifies thisdata, hence it would
249         # lead to pollution cooker.configuration.data, which is
250         # why we use it on a safe copy we obtained from cooker right after
251         # parsing the initial *.conf files
252         try:
253             bbfile_data = parse.handle( bf, thisdata )
254         except parse.ParseError:
255             print "ERROR: Unable to open or parse '%s'" % bf
256         else:
257             item = data.getVar('PN', bbfile_data, 1)
258             data.setVar( "_task_cache", [], bbfile_data ) # force
259             try:
260                 cooker.tryBuildPackage( os.path.abspath( bf ), item, cmd, bbfile_data, True )
261             except build.EventException, e:
262                 print "ERROR: Couldn't build '%s'" % name
263                 global last_exception
264                 last_exception = e
265
266         cooker.configuration.cmd = oldcmd
267     fileBuild.usage = "<bbfile>"
268
269     def fileClean( self, params ):
270         """Clean a .bb file"""
271         self.fileBuild( params, "clean" )
272     fileClean.usage = "<bbfile>"
273
274     def fileEdit( self, params ):
275         """Call $EDITOR on a .bb file"""
276         name = params[0]
277         os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
278     fileEdit.usage = "<bbfile>"
279
280     def fileRebuild( self, params ):
281         """Rebuild (clean & build) a .bb file"""
282         self.fileBuild( params, "rebuild" )
283     fileRebuild.usage = "<bbfile>"
284
285     def fileReparse( self, params ):
286         """(re)Parse a bb file"""
287         bbfile = params[0]
288         print "SHELL: Parsing '%s'" % bbfile
289         parse.update_mtime( bbfile )
290         cooker.bb_cache.cacheValidUpdate(bbfile)
291         fromCache = cooker.bb_cache.loadData(bbfile, cooker.configuration.data)
292         cooker.bb_cache.sync()
293         if False: #fromCache:
294             print "SHELL: File has not been updated, not reparsing"
295         else:
296             print "SHELL: Parsed"
297     fileReparse.usage = "<bbfile>"
298
299     def abort( self, params ):
300         """Toggle abort task execution flag (see bitbake -k)"""
301         cooker.configuration.abort = not cooker.configuration.abort
302         print "SHELL: Abort Flag is now '%s'" % repr( cooker.configuration.abort )
303
304     def force( self, params ):
305         """Toggle force task execution flag (see bitbake -f)"""
306         cooker.configuration.force = not cooker.configuration.force
307         print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
308
309     def help( self, params ):
310         """Show a comprehensive list of commands and their purpose"""
311         print "="*30, "Available Commands", "="*30
312         allcmds = cmds.keys()
313         allcmds.sort()
314         for cmd in allcmds:
315             function,numparams,usage,helptext = cmds[cmd]
316             print "| %s | %s" % (usage.ljust(30), helptext)
317         print "="*78
318
319     def lastError( self, params ):
320         """Show the reason or log that was produced by the last BitBake event exception"""
321         if last_exception is None:
322             print "SHELL: No Errors yet (Phew)..."
323         else:
324             reason, event = last_exception.args
325             print "SHELL: Reason for the last error: '%s'" % reason
326             if ':' in reason:
327                 msg, filename = reason.split( ':' )
328                 filename = filename.strip()
329                 print "SHELL: Dumping log file for last error:"
330                 try:
331                     print open( filename ).read()
332                 except IOError:
333                     print "ERROR: Couldn't open '%s'" % filename
334
335     def match( self, params ):
336         """Dump all files or providers matching a glob expression"""
337         what, globexpr = params
338         if what == "files":
339             self._checkParsed()
340             for key in globfilter( cooker.status.pkg_fn.keys(), globexpr ): print key
341         elif what == "providers":
342             self._checkParsed()
343             for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
344         else:
345             print "Usage: match %s" % self.print_.usage
346     match.usage = "<files|providers> <glob>"
347
348     def new( self, params ):
349         """Create a new .bb file and open the editor"""
350         dirname, filename = params
351         packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
352         fulldirname = "%s/%s" % ( packages, dirname )
353
354         if not os.path.exists( fulldirname ):
355             print "SHELL: Creating '%s'" % fulldirname
356             os.mkdir( fulldirname )
357         if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
358             if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
359                 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
360                 return False
361             print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
362             newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
363             print >>newpackage,"""DESCRIPTION = ""
364 SECTION = ""
365 AUTHOR = ""
366 HOMEPAGE = ""
367 MAINTAINER = ""
368 LICENSE = "GPL"
369 PR = "r0"
370
371 SRC_URI = ""
372
373 #inherit base
374
375 #do_configure() {
376 #
377 #}
378
379 #do_compile() {
380 #
381 #}
382
383 #do_stage() {
384 #
385 #}
386
387 #do_install() {
388 #
389 #}
390 """
391             newpackage.close()
392             os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
393     new.usage = "<directory> <filename>"
394
395     def pasteBin( self, params ):
396         """Send a command + output buffer to the pastebin at http://rafb.net/paste"""
397         index = params[0]
398         contents = self._shell.myout.buffer( int( index ) )
399         sendToPastebin( "output of " + params[0], contents )
400     pasteBin.usage = "<index>"
401
402     def pasteLog( self, params ):
403         """Send the last event exception error log (if there is one) to http://rafb.net/paste"""
404         if last_exception is None:
405             print "SHELL: No Errors yet (Phew)..."
406         else:
407             reason, event = last_exception.args
408             print "SHELL: Reason for the last error: '%s'" % reason
409             if ':' in reason:
410                 msg, filename = reason.split( ':' )
411                 filename = filename.strip()
412                 print "SHELL: Pasting log file to pastebin..."
413
414                 file = open( filename ).read()
415                 sendToPastebin( "contents of " + filename, file )
416
417     def patch( self, params ):
418         """Execute 'patch' command on a providee"""
419         self.build( params, "patch" )
420     patch.usage = "<providee>"
421
422     def parse( self, params ):
423         """(Re-)parse .bb files and calculate the dependency graph"""
424         cooker.status = cache.CacheData()
425         ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
426         cooker.status.ignored_dependencies = set( ignore.split() )
427         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
428
429         (filelist, masked) = cooker.collect_bbfiles()
430         cooker.parse_bbfiles(filelist, masked, cooker.myProgressCallback)
431         cooker.buildDepgraph()
432         global parsed
433         parsed = True
434         print
435
436     def reparse( self, params ):
437         """(re)Parse a providee's bb file"""
438         bbfile = self._findProvider( params[0] )
439         if bbfile is not None:
440             print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
441             self.fileReparse( [ bbfile ] )
442         else:
443             print "ERROR: Nothing provides '%s'" % params[0]
444     reparse.usage = "<providee>"
445
446     def getvar( self, params ):
447         """Dump the contents of an outer BitBake environment variable"""
448         var = params[0]
449         value = data.getVar( var, cooker.configuration.data, 1 )
450         print value
451     getvar.usage = "<variable>"
452
453     def peek( self, params ):
454         """Dump contents of variable defined in providee's metadata"""
455         name, var = params
456         bbfile = self._findProvider( name )
457         if bbfile is not None:
458             the_data = cooker.bb_cache.loadDataFull(bbfile, cooker.configuration.data)
459             value = the_data.getVar( var, 1 )
460             print value
461         else:
462             print "ERROR: Nothing provides '%s'" % name
463     peek.usage = "<providee> <variable>"
464
465     def poke( self, params ):
466         """Set contents of variable defined in providee's metadata"""
467         name, var, value = params
468         bbfile = self._findProvider( name )
469         if bbfile is not None:
470             print "ERROR: Sorry, this functionality is currently broken"
471             #d = cooker.pkgdata[bbfile]
472             #data.setVar( var, value, d )
473
474             # mark the change semi persistant
475             #cooker.pkgdata.setDirty(bbfile, d)
476             #print "OK"
477         else:
478             print "ERROR: Nothing provides '%s'" % name
479     poke.usage = "<providee> <variable> <value>"
480
481     def print_( self, params ):
482         """Dump all files or providers"""
483         what = params[0]
484         if what == "files":
485             self._checkParsed()
486             for key in cooker.status.pkg_fn.keys(): print key
487         elif what == "providers":
488             self._checkParsed()
489             for key in cooker.status.providers.keys(): print key
490         else:
491             print "Usage: print %s" % self.print_.usage
492     print_.usage = "<files|providers>"
493
494     def python( self, params ):
495         """Enter the expert mode - an interactive BitBake Python Interpreter"""
496         sys.ps1 = "EXPERT BB>>> "
497         sys.ps2 = "EXPERT BB... "
498         import code
499         interpreter = code.InteractiveConsole( dict( globals() ) )
500         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
501
502     def showdata( self, params ):
503         """Execute 'showdata' on a providee"""
504         self.build( params, "showdata" )
505     showdata.usage = "<providee>"
506
507     def setVar( self, params ):
508         """Set an outer BitBake environment variable"""
509         var, value = params
510         data.setVar( var, value, cooker.configuration.data )
511         print "OK"
512     setVar.usage = "<variable> <value>"
513
514     def rebuild( self, params ):
515         """Clean and rebuild a .bb file or a providee"""
516         self.build( params, "clean" )
517         self.build( params, "build" )
518     rebuild.usage = "<providee>"
519
520     def shell( self, params ):
521         """Execute a shell command and dump the output"""
522         if params != "":
523             print commands.getoutput( " ".join( params ) )
524     shell.usage = "<...>"
525
526     def stage( self, params ):
527         """Execute 'stage' on a providee"""
528         self.build( params, "stage" )
529     stage.usage = "<providee>"
530
531     def status( self, params ):
532         """<just for testing>"""
533         print "-" * 78
534         print "build cache = '%s'" % cooker.build_cache
535         print "build cache fail = '%s'" % cooker.build_cache_fail
536         print "building list = '%s'" % cooker.building_list
537         print "build path = '%s'" % cooker.build_path
538         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
539         print "build stats = '%s'" % cooker.stats
540         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
541         print "memory output contents = '%s'" % self._shell.myout._buffer
542
543     def test( self, params ):
544         """<just for testing>"""
545         print "testCommand called with '%s'" % params
546
547     def unpack( self, params ):
548         """Execute 'unpack' on a providee"""
549         self.build( params, "unpack" )
550     unpack.usage = "<providee>"
551
552     def which( self, params ):
553         """Computes the providers for a given providee"""
554         item = params[0]
555
556         self._checkParsed()
557
558         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
559         if not preferred: preferred = item
560
561         try:
562             lv, lf, pv, pf = Providers.findBestProvider(preferred, cooker.configuration.data, cooker.status, 
563 cooker.build_cache_fail)
564         except KeyError:
565             lv, lf, pv, pf = (None,)*4
566
567         try:
568             providers = cooker.status.providers[item]
569         except KeyError:
570             print "SHELL: ERROR: Nothing provides", preferred
571         else:
572             for provider in providers:
573                 if provider == pf: provider = " (***) %s" % provider
574                 else:              provider = "       %s" % provider
575                 print provider
576     which.usage = "<providee>"
577
578 ##########################################################################
579 # Common helper functions
580 ##########################################################################
581
582 def completeFilePath( bbfile ):
583     """Get the complete bbfile path"""
584     if not cooker.status.pkg_fn: return bbfile
585     for key in cooker.status.pkg_fn.keys():
586         if key.endswith( bbfile ):
587             return key
588     return bbfile
589
590 def sendToPastebin( desc, content ):
591     """Send content to http://oe.pastebin.com"""
592     mydata = {}
593     mydata["lang"] = "Plain Text"
594     mydata["desc"] = desc
595     mydata["cvt_tabs"] = "No"
596     mydata["nick"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
597     mydata["text"] = content
598     params = urllib.urlencode( mydata )
599     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
600
601     host = "rafb.net"
602     conn = httplib.HTTPConnection( "%s:80" % host )
603     conn.request("POST", "/paste/paste.php", params, headers )
604
605     response = conn.getresponse()
606     conn.close()
607
608     if response.status == 302:
609         location = response.getheader( "location" ) or "unknown"
610         print "SHELL: Pasted to http://%s%s" % ( host, location )
611     else:
612         print "ERROR: %s %s" % ( response.status, response.reason )
613
614 def completer( text, state ):
615     """Return a possible readline completion"""
616     debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
617
618     if state == 0:
619         line = readline.get_line_buffer()
620         if " " in line:
621             line = line.split()
622             # we are in second (or more) argument
623             if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
624                 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
625                 if u == "<variable>":
626                     allmatches = cooker.configuration.data.keys()
627                 elif u == "<bbfile>":
628                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
629                     else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
630                 elif u == "<providee>":
631                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
632                     else: allmatches = cooker.status.providers.iterkeys()
633                 else: allmatches = [ "(No tab completion available for this command)" ]
634             else: allmatches = [ "(No tab completion available for this command)" ]
635         else:
636             # we are in first argument
637             allmatches = cmds.iterkeys()
638
639         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
640         #print "completer.matches = '%s'" % completer.matches
641     if len( completer.matches ) > state:
642         return completer.matches[state]
643     else:
644         return None
645
646 def debugOut( text ):
647     if debug:
648         sys.stderr.write( "( %s )\n" % text )
649
650 def columnize( alist, width = 80 ):
651     """
652     A word-wrap function that preserves existing line breaks
653     and most spaces in the text. Expects that existing line
654     breaks are posix newlines (\n).
655     """
656     return reduce(lambda line, word, width=width: '%s%s%s' %
657                   (line,
658                    ' \n'[(len(line[line.rfind('\n')+1:])
659                          + len(word.split('\n',1)[0]
660                               ) >= width)],
661                    word),
662                   alist
663                  )
664
665 def globfilter( names, pattern ):
666     return fnmatch.filter( names, pattern )
667
668 ##########################################################################
669 # Class MemoryOutput
670 ##########################################################################
671
672 class MemoryOutput:
673     """File-like output class buffering the output of the last 10 commands"""
674     def __init__( self, delegate ):
675         self.delegate = delegate
676         self._buffer = []
677         self.text = []
678         self._command = None
679
680     def startCommand( self, command ):
681         self._command = command
682         self.text = []
683     def endCommand( self ):
684         if self._command is not None:
685             if len( self._buffer ) == 10: del self._buffer[0]
686             self._buffer.append( ( self._command, self.text ) )
687     def removeLast( self ):
688         if self._buffer:
689             del self._buffer[ len( self._buffer ) - 1 ]
690         self.text = []
691         self._command = None
692     def lastBuffer( self ):
693         if self._buffer:
694             return self._buffer[ len( self._buffer ) -1 ][1]
695     def bufferedCommands( self ):
696         return [ cmd for cmd, output in self._buffer ]
697     def buffer( self, i ):
698         if i < len( self._buffer ):
699             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
700         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
701     def write( self, text ):
702         if self._command is not None and text != "BB>> ": self.text.append( text )
703         if self.delegate is not None: self.delegate.write( text )
704     def flush( self ):
705         return self.delegate.flush()
706     def fileno( self ):
707         return self.delegate.fileno()
708     def isatty( self ):
709         return self.delegate.isatty()
710
711 ##########################################################################
712 # Class BitBakeShell
713 ##########################################################################
714
715 class BitBakeShell:
716
717     def __init__( self ):
718         """Register commands and set up readline"""
719         self.commandQ = Queue.Queue()
720         self.commands = BitBakeShellCommands( self )
721         self.myout = MemoryOutput( sys.stdout )
722         self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
723         self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
724
725         readline.set_completer( completer )
726         readline.set_completer_delims( " " )
727         readline.parse_and_bind("tab: complete")
728
729         try:
730             readline.read_history_file( self.historyfilename )
731         except IOError:
732             pass  # It doesn't exist yet.
733
734         print __credits__
735
736         # save initial cooker configuration (will be reused in file*** commands)
737         global initdata
738         initdata = copy.deepcopy( cooker.configuration.data )
739
740     def cleanup( self ):
741         """Write readline history and clean up resources"""
742         debugOut( "writing command history" )
743         try:
744             readline.write_history_file( self.historyfilename )
745         except:
746             print "SHELL: Unable to save command history"
747
748     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
749         """Register a command"""
750         if usage == "": usage = command
751         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
752         cmds[command] = ( function, numparams, usage, helptext )
753
754     def processCommand( self, command, params ):
755         """Process a command. Check number of params and print a usage string, if appropriate"""
756         debugOut( "processing command '%s'..." % command )
757         try:
758             function, numparams, usage, helptext = cmds[command]
759         except KeyError:
760             print "SHELL: ERROR: '%s' command is not a valid command." % command
761             self.myout.removeLast()
762         else:
763             if (numparams != -1) and (not len( params ) == numparams):
764                 print "Usage: '%s'" % usage
765                 return
766
767             result = function( self.commands, params )
768             debugOut( "result was '%s'" % result )
769
770     def processStartupFile( self ):
771         """Read and execute all commands found in $HOME/.bbsh_startup"""
772         if os.path.exists( self.startupfilename ):
773             startupfile = open( self.startupfilename, "r" )
774             for cmdline in startupfile:
775                 debugOut( "processing startup line '%s'" % cmdline )
776                 if not cmdline:
777                     continue
778                 if "|" in cmdline:
779                     print "ERROR: '|' in startup file is not allowed. Ignoring line"
780                     continue
781                 self.commandQ.put( cmdline.strip() )
782
783     def main( self ):
784         """The main command loop"""
785         while not leave_mainloop:
786             try:
787                 if self.commandQ.empty():
788                     sys.stdout = self.myout.delegate
789                     cmdline = raw_input( "BB>> " )
790                     sys.stdout = self.myout
791                 else:
792                     cmdline = self.commandQ.get()
793                 if cmdline:
794                     allCommands = cmdline.split( ';' )
795                     for command in allCommands:
796                         pipecmd = None
797                         #
798                         # special case for expert mode
799                         if command == 'python':
800                             sys.stdout = self.myout.delegate
801                             self.processCommand( command, "" )
802                             sys.stdout = self.myout
803                         else:
804                             self.myout.startCommand( command )
805                             if '|' in command: # disable output
806                                 command, pipecmd = command.split( '|' )
807                                 delegate = self.myout.delegate
808                                 self.myout.delegate = None
809                             tokens = shlex.split( command, True )
810                             self.processCommand( tokens[0], tokens[1:] or "" )
811                             self.myout.endCommand()
812                             if pipecmd is not None: # restore output
813                                 self.myout.delegate = delegate
814
815                                 pipe = popen2.Popen4( pipecmd )
816                                 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
817                                 pipe.tochild.close()
818                                 sys.stdout.write( pipe.fromchild.read() )
819                         #
820             except EOFError:
821                 print
822                 return
823             except KeyboardInterrupt:
824                 print
825
826 ##########################################################################
827 # Start function - called from the BitBake command line utility
828 ##########################################################################
829
830 def start( aCooker ):
831     global cooker
832     cooker = aCooker
833     bbshell = BitBakeShell()
834     bbshell.processStartupFile()
835     bbshell.main()
836     bbshell.cleanup()
837
838 if __name__ == "__main__":
839     print "SHELL: Sorry, this program should only be called by BitBake."