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