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