bitbake/lib/bb/providers.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, cooker.configuration.verbose)
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, bbfile_data )
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 http://oe.pastebin.com"""
368         index = params[0]
369         contents = self._shell.myout.buffer( int( index ) )
370         status, error, location = sendToPastebin( contents )
371         if status == 302:
372             print "SHELL: Pasted to %s" % location
373         else:
374             print "ERROR: %s %s" % ( status, error )
375     pasteBin.usage = "<index>"
376
377     def pasteLog( self, params ):
378         """Send the last event exception error log (if there is one) to http://oe.pastebin.com"""
379         if last_exception is None:
380             print "SHELL: No Errors yet (Phew)..."
381         else:
382             reason, event = last_exception.args
383             print "SHELL: Reason for the last error: '%s'" % reason
384             if ':' in reason:
385                 msg, filename = reason.split( ':' )
386                 filename = filename.strip()
387                 print "SHELL: Pasting log file to pastebin..."
388
389                 status, error, location = sendToPastebin( open( filename ).read() )
390
391                 if status == 302:
392                     print "SHELL: Pasted to %s" % location
393                 else:
394                     print "ERROR: %s %s" % ( status, error )
395
396     def patch( self, params ):
397         """Execute 'patch' command on a providee"""
398         self.build( params, "patch" )
399     patch.usage = "<providee>"
400
401     def parse( self, params ):
402         """(Re-)parse .bb files and calculate the dependency graph"""
403         cooker.status = cooker.ParsingStatus()
404         ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
405         cooker.status.ignored_dependencies = set( ignore.split() )
406         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
407
408         cooker.collect_bbfiles( cooker.myProgressCallback )
409         cooker.buildDepgraph()
410         global parsed
411         parsed = True
412         print
413
414     def reparse( self, params ):
415         """(re)Parse a providee's bb file"""
416         bbfile = self._findProvider( params[0] )
417         if bbfile is not None:
418             print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
419             self.fileReparse( [ bbfile ] )
420         else:
421             print "ERROR: Nothing provides '%s'" % params[0]
422     reparse.usage = "<providee>"
423
424     def getvar( self, params ):
425         """Dump the contents of an outer BitBake environment variable"""
426         var = params[0]
427         value = data.getVar( var, cooker.configuration.data, 1 )
428         print value
429     getvar.usage = "<variable>"
430
431     def peek( self, params ):
432         """Dump contents of variable defined in providee's metadata"""
433         name, var = params
434         bbfile = self._findProvider( name )
435         if bbfile is not None:
436             the_data = cooker.bb_cache.loadDataFull(bbfile, cooker)
437             value = the_data.getVar( var, 1 )
438             print value
439         else:
440             print "ERROR: Nothing provides '%s'" % name
441     peek.usage = "<providee> <variable>"
442
443     def poke( self, params ):
444         """Set contents of variable defined in providee's metadata"""
445         name, var, value = params
446         bbfile = self._findProvider( name )
447         if bbfile is not None:
448             print "ERROR: Sorry, this functionality is currently broken"
449             #d = cooker.pkgdata[bbfile]
450             #data.setVar( var, value, d )
451
452             # mark the change semi persistant
453             #cooker.pkgdata.setDirty(bbfile, d)
454             #print "OK"
455         else:
456             print "ERROR: Nothing provides '%s'" % name
457     poke.usage = "<providee> <variable> <value>"
458
459     def print_( self, params ):
460         """Dump all files or providers"""
461         what = params[0]
462         if what == "files":
463             self._checkParsed()
464             for key in cooker.status.pkg_fn.keys(): print key
465         elif what == "providers":
466             self._checkParsed()
467             for key in cooker.status.providers.keys(): print key
468         else:
469             print "Usage: print %s" % self.print_.usage
470     print_.usage = "<files|providers>"
471
472     def python( self, params ):
473         """Enter the expert mode - an interactive BitBake Python Interpreter"""
474         sys.ps1 = "EXPERT BB>>> "
475         sys.ps2 = "EXPERT BB... "
476         import code
477         interpreter = code.InteractiveConsole( dict( globals() ) )
478         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
479
480     def showdata( self, params ):
481         """Execute 'showdata' on a providee"""
482         self.build( params, "showdata" )
483     showdata.usage = "<providee>"
484
485     def setVar( self, params ):
486         """Set an outer BitBake environment variable"""
487         var, value = params
488         data.setVar( var, value, cooker.configuration.data )
489         print "OK"
490     setVar.usage = "<variable> <value>"
491
492     def rebuild( self, params ):
493         """Clean and rebuild a .bb file or a providee"""
494         self.build( params, "clean" )
495         self.build( params, "build" )
496     rebuild.usage = "<providee>"
497
498     def shell( self, params ):
499         """Execute a shell command and dump the output"""
500         if params != "":
501             print commands.getoutput( " ".join( params ) )
502     shell.usage = "<...>"
503
504     def stage( self, params ):
505         """Execute 'stage' on a providee"""
506         self.build( params, "stage" )
507     stage.usage = "<providee>"
508
509     def status( self, params ):
510         """<just for testing>"""
511         print "-" * 78
512         print "build cache = '%s'" % cooker.build_cache
513         print "build cache fail = '%s'" % cooker.build_cache_fail
514         print "building list = '%s'" % cooker.building_list
515         print "build path = '%s'" % cooker.build_path
516         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
517         print "build stats = '%s'" % cooker.stats
518         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
519         print "memory output contents = '%s'" % self._shell.myout._buffer
520
521     def test( self, params ):
522         """<just for testing>"""
523         print "testCommand called with '%s'" % params
524
525     def unpack( self, params ):
526         """Execute 'unpack' on a providee"""
527         self.build( params, "unpack" )
528     unpack.usage = "<providee>"
529
530     def which( self, params ):
531         """Computes the providers for a given providee"""
532         item = params[0]
533
534         self._checkParsed()
535
536         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
537         if not preferred: preferred = item
538
539         try:
540             lv, lf, pv, pf = bb.providers.findBestProvider(preferred, cooker.configuration.data, cooker.status, cooker.build_cache_fail, cooker.configuration.verbose)
541         except KeyError:
542             lv, lf, pv, pf = (None,)*4
543
544         try:
545             providers = cooker.status.providers[item]
546         except KeyError:
547             print "SHELL: ERROR: Nothing provides", preferred
548         else:
549             for provider in providers:
550                 if provider == pf: provider = " (***) %s" % provider
551                 else:              provider = "       %s" % provider
552                 print provider
553     which.usage = "<providee>"
554
555 ##########################################################################
556 # Common helper functions
557 ##########################################################################
558
559 def completeFilePath( bbfile ):
560     """Get the complete bbfile path"""
561     if not cooker.status.pkg_fn: return bbfile
562     for key in cooker.status.pkg_fn.keys():
563         if key.endswith( bbfile ):
564             return key
565     return bbfile
566
567 def sendToPastebin( content ):
568     """Send content to http://oe.pastebin.com"""
569     mydata = {}
570     mydata["parent_pid"] = ""
571     mydata["format"] = "bash"
572     mydata["code2"] = content
573     mydata["paste"] = "Send"
574     mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
575     params = urllib.urlencode( mydata )
576     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
577
578     conn = httplib.HTTPConnection( "oe.pastebin.com:80" )
579     conn.request("POST", "/", params, headers )
580
581     response = conn.getresponse()
582     conn.close()
583
584     return response.status, response.reason, response.getheader( "location" ) or "unknown"
585
586 def completer( text, state ):
587     """Return a possible readline completion"""
588     debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
589
590     if state == 0:
591         line = readline.get_line_buffer()
592         if " " in line:
593             line = line.split()
594             # we are in second (or more) argument
595             if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
596                 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
597                 if u == "<variable>":
598                     allmatches = cooker.configuration.data.keys()
599                 elif u == "<bbfile>":
600                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
601                     else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
602                 elif u == "<providee>":
603                     if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
604                     else: allmatches = cooker.status.providers.iterkeys()
605                 else: allmatches = [ "(No tab completion available for this command)" ]
606             else: allmatches = [ "(No tab completion available for this command)" ]
607         else:
608             # we are in first argument
609             allmatches = cmds.iterkeys()
610
611         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
612         #print "completer.matches = '%s'" % completer.matches
613     if len( completer.matches ) > state:
614         return completer.matches[state]
615     else:
616         return None
617
618 def debugOut( text ):
619     if debug:
620         sys.stderr.write( "( %s )\n" % text )
621
622 def columnize( alist, width = 80 ):
623     """
624     A word-wrap function that preserves existing line breaks
625     and most spaces in the text. Expects that existing line
626     breaks are posix newlines (\n).
627     """
628     return reduce(lambda line, word, width=width: '%s%s%s' %
629                   (line,
630                    ' \n'[(len(line[line.rfind('\n')+1:])
631                          + len(word.split('\n',1)[0]
632                               ) >= width)],
633                    word),
634                   alist
635                  )
636
637 def globfilter( names, pattern ):
638     return fnmatch.filter( names, pattern )
639
640 ##########################################################################
641 # Class MemoryOutput
642 ##########################################################################
643
644 class MemoryOutput:
645     """File-like output class buffering the output of the last 10 commands"""
646     def __init__( self, delegate ):
647         self.delegate = delegate
648         self._buffer = []
649         self.text = []
650         self._command = None
651
652     def startCommand( self, command ):
653         self._command = command
654         self.text = []
655     def endCommand( self ):
656         if self._command is not None:
657             if len( self._buffer ) == 10: del self._buffer[0]
658             self._buffer.append( ( self._command, self.text ) )
659     def removeLast( self ):
660         if self._buffer:
661             del self._buffer[ len( self._buffer ) - 1 ]
662         self.text = []
663         self._command = None
664     def lastBuffer( self ):
665         if self._buffer:
666             return self._buffer[ len( self._buffer ) -1 ][1]
667     def bufferedCommands( self ):
668         return [ cmd for cmd, output in self._buffer ]
669     def buffer( self, i ):
670         if i < len( self._buffer ):
671             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
672         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
673     def write( self, text ):
674         if self._command is not None and text != "BB>> ": self.text.append( text )
675         if self.delegate is not None: self.delegate.write( text )
676     def flush( self ):
677         return self.delegate.flush()
678     def fileno( self ):
679         return self.delegate.fileno()
680     def isatty( self ):
681         return self.delegate.isatty()
682
683 ##########################################################################
684 # Class BitBakeShell
685 ##########################################################################
686
687 class BitBakeShell:
688
689     def __init__( self ):
690         """Register commands and set up readline"""
691         self.commandQ = Queue.Queue()
692         self.commands = BitBakeShellCommands( self )
693         self.myout = MemoryOutput( sys.stdout )
694         self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
695         self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
696
697         readline.set_completer( completer )
698         readline.set_completer_delims( " " )
699         readline.parse_and_bind("tab: complete")
700
701         try:
702             readline.read_history_file( self.historyfilename )
703         except IOError:
704             pass  # It doesn't exist yet.
705
706         print __credits__
707
708         # save initial cooker configuration (will be reused in file*** commands)
709         global initdata
710         initdata = copy.deepcopy( cooker.configuration.data )
711
712     def cleanup( self ):
713         """Write readline history and clean up resources"""
714         debugOut( "writing command history" )
715         try:
716             readline.write_history_file( self.historyfilename )
717         except:
718             print "SHELL: Unable to save command history"
719
720     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
721         """Register a command"""
722         if usage == "": usage = command
723         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
724         cmds[command] = ( function, numparams, usage, helptext )
725
726     def processCommand( self, command, params ):
727         """Process a command. Check number of params and print a usage string, if appropriate"""
728         debugOut( "processing command '%s'..." % command )
729         try:
730             function, numparams, usage, helptext = cmds[command]
731         except KeyError:
732             print "SHELL: ERROR: '%s' command is not a valid command." % command
733             self.myout.removeLast()
734         else:
735             if (numparams != -1) and (not len( params ) == numparams):
736                 print "Usage: '%s'" % usage
737                 return
738
739             result = function( self.commands, params )
740             debugOut( "result was '%s'" % result )
741
742     def processStartupFile( self ):
743         """Read and execute all commands found in $HOME/.bbsh_startup"""
744         if os.path.exists( self.startupfilename ):
745             startupfile = open( self.startupfilename, "r" )
746             for cmdline in startupfile:
747                 debugOut( "processing startup line '%s'" % cmdline )
748                 if not cmdline:
749                     continue
750                 if "|" in cmdline:
751                     print "ERROR: '|' in startup file is not allowed. Ignoring line"
752                     continue
753                 self.commandQ.put( cmdline.strip() )
754
755     def main( self ):
756         """The main command loop"""
757         while not leave_mainloop:
758             try:
759                 if self.commandQ.empty():
760                     sys.stdout = self.myout.delegate
761                     cmdline = raw_input( "BB>> " )
762                     sys.stdout = self.myout
763                 else:
764                     cmdline = self.commandQ.get()
765                 if cmdline:
766                     allCommands = cmdline.split( ';' )
767                     for command in allCommands:
768                         pipecmd = None
769                         #
770                         # special case for expert mode
771                         if command == 'python':
772                             sys.stdout = self.myout.delegate
773                             self.processCommand( command, "" )
774                             sys.stdout = self.myout
775                         else:
776                             self.myout.startCommand( command )
777                             if '|' in command: # disable output
778                                 command, pipecmd = command.split( '|' )
779                                 delegate = self.myout.delegate
780                                 self.myout.delegate = None
781                             tokens = shlex.split( command, True )
782                             self.processCommand( tokens[0], tokens[1:] or "" )
783                             self.myout.endCommand()
784                             if pipecmd is not None: # restore output
785                                 self.myout.delegate = delegate
786
787                                 pipe = popen2.Popen4( pipecmd )
788                                 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
789                                 pipe.tochild.close()
790                                 sys.stdout.write( pipe.fromchild.read() )
791                         #
792             except EOFError:
793                 print
794                 return
795             except KeyboardInterrupt:
796                 print
797
798 ##########################################################################
799 # Start function - called from the BitBake command line utility
800 ##########################################################################
801
802 def start( aCooker ):
803     global cooker
804     cooker = aCooker
805     bbshell = BitBakeShell()
806     bbshell.processStartupFile()
807     bbshell.main()
808     bbshell.cleanup()
809
810 if __name__ == "__main__":
811     print "SHELL: Sorry, this program should only be called by BitBake."