2 # ex:ts=4:sw=4:sts=4:et
3 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4 ##########################################################################
6 # Copyright (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>, Vanille Media
8 # This program is free software; you can redistribute it and/or modify it under
9 # the terms of the GNU General Public License as published by the Free Software
10 # Foundation; version 2 of the License.
12 # This program is distributed in the hope that it will be useful, but WITHOUT
13 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
14 # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details.
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc., 59 Temple
18 # Place, Suite 330, Boston, MA 02111-1307 USA.
20 ##########################################################################
28 * command to reparse just one (or more) bbfile(s)
29 * automatic check if reparsing is necessary (inotify?)
30 * frontend for bb file manipulation?
31 * pipe output of commands into shell commands (i.e grep or sort)?
32 * job control, i.e. bring commands into background with '&', fg, bg, etc.?
33 * start parsing in background right after startup?
34 * print variable from package data
35 * command aliases / shortcuts?
38 ##########################################################################
39 # Import and setup global variables
40 ##########################################################################
45 from sets import Set as set
46 import sys, os, imp, readline, socket, httplib, urllib, commands
47 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
48 from bb import data, parse, build, make, fatal
51 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
52 Type 'help' for more information, press CTRL-D to exit.""" % __version__
55 leave_mainloop = False
59 debug = os.environ.get( "BBSHELL_DEBUG", "" ) != ""
60 history_file = "%s/.bbsh_history" % os.environ.get( "HOME" )
62 ##########################################################################
63 # Class BitBakeShellCommands
64 ##########################################################################
66 class BitBakeShellCommands:
67 """This class contains the valid commands for the shell"""
69 def __init__( self, shell ):
70 """Register all the commands"""
72 for attr in BitBakeShellCommands.__dict__:
73 if not attr.startswith( "__" ):
74 if attr.endswith( "_" ):
75 command = attr[:-1].lower()
77 command = attr[:].lower()
78 method = getattr( BitBakeShellCommands, attr )
79 if debug: print "registering command '%s'" % command
80 # scan number of arguments
81 usage = getattr( method, "usage", "" )
83 numArgs = len( usage.split() )
86 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
88 def buffer( self, params ):
89 """Dump specified output buffer"""
91 print self._shell.myout.buffer( int( index ) )
92 buffer.usage = "<index>"
94 def buffers( self, params ):
95 """Show the available output buffers"""
96 commands = self._shell.myout.bufferedCommands()
98 print "SHELL: No buffered commands available yet. Start doing something."
100 print "="*35, "Available Output Buffers", "="*27
101 for index, cmd in enumerate( commands ):
102 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
105 def build( self, params, cmd = "build" ):
106 """Build a providee"""
109 oldcmd = make.options.cmd
110 make.options.cmd = cmd
111 cooker.build_cache = []
112 cooker.build_cache_fail = []
115 print "SHELL: D'oh! The .bb files haven't been parsed yet. Next time call 'parse' before building stuff. This time I'll do it for 'ya."
118 cooker.buildProvider( name )
119 except build.EventException, e:
120 print "ERROR: Couldn't build '%s'" % name
121 global last_exception
124 make.options.cmd = oldcmd
125 build.usage = "<providee>"
127 def clean( self, params ):
128 """Clean a providee"""
129 self.build( params, "clean" )
130 clean.usage = "<providee>"
132 def edit( self, params ):
133 """Call $EDITOR on a .bb file"""
135 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
136 edit.usage = "<bbfile>"
138 def environment( self, params ):
139 """Dump out the outer BitBake environment (see bbread)"""
140 data.emit_env(sys.__stdout__, make.cfg, True)
142 def exit_( self, params ):
143 """Leave the BitBake Shell"""
144 if debug: print "(setting leave_mainloop to true)"
145 global leave_mainloop
146 leave_mainloop = True
148 def fileBuild( self, params, cmd = "build" ):
149 """Parse and build a .bb file"""
151 bf = completeFilePath( name )
152 print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
154 oldcmd = make.options.cmd
155 make.options.cmd = cmd
156 cooker.build_cache = []
157 cooker.build_cache_fail = []
160 bbfile_data = parse.handle( bf, make.cfg )
161 except parse.ParseError:
162 print "ERROR: Unable to open or parse '%s'" % bf
164 item = data.getVar('PN', bbfile_data, 1)
165 data.setVar( "_task_cache", [], bbfile_data ) # force
167 cooker.tryBuildPackage( os.path.abspath( bf ), item, bbfile_data )
168 except build.EventException, e:
169 print "ERROR: Couldn't build '%s'" % name
170 global last_exception
173 make.options.cmd = oldcmd
174 fileBuild.usage = "<bbfile>"
176 def fileClean( self, params ):
177 """Clean a .bb file"""
178 self.fileBuild( params, "clean" )
179 fileClean.usage = "<bbfile>"
181 def fileRebuild( self, params ):
182 """Rebuild (clean & build) a .bb file"""
183 self.fileClean( params )
184 self.fileBuild( params )
185 fileRebuild.usage = "<bbfile>"
187 def help( self, params ):
188 """Show a comprehensive list of commands and their purpose"""
189 print "="*30, "Available Commands", "="*30
190 allcmds = cmds.keys()
193 function,numparams,usage,helptext = cmds[cmd]
194 print "| %s | %s" % (usage.ljust(30), helptext)
197 def lastError( self, params ):
198 """Show the reason or log that was produced by the last BitBake event exception"""
199 if last_exception is None:
200 print "SHELL: No Errors yet (Phew)..."
202 reason, event = last_exception.args
203 print "SHELL: Reason for the last error: '%s'" % reason
205 msg, filename = reason.split( ':' )
206 filename = filename.strip()
207 print "SHELL: Dumping log file for last error:"
209 print open( filename ).read()
211 print "ERROR: Couldn't open '%s'" % filename
213 def new( self, params ):
214 """Create a new .bb file and open the editor"""
215 dirname, filename = params
216 packages = '/'.join( data.getVar( "BBFILES", make.cfg, 1 ).split('/')[:-2] )
217 fulldirname = "%s/%s" % ( packages, dirname )
219 if not os.path.exists( fulldirname ):
220 print "SHELL: Creating '%s'" % fulldirname
221 os.mkdir( fulldirname )
222 if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
223 if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
224 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
226 print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
227 newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
228 print >>newpackage,"""DESCRIPTION = ""
257 os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
258 new.usage = "<directory> <filename>"
260 def pasteBin( self, params ):
261 """Send a command + output buffer to http://pastebin.com"""
263 contents = self._shell.myout.buffer( int( index ) )
264 status, error, location = sendToPastebin( contents )
266 print "SHELL: Pasted to %s" % location
268 print "ERROR: %s %s" % ( response.status, response.reason )
269 pasteBin.usage = "<index>"
271 def pasteLog( self, params ):
272 """Send the last event exception error log (if there is one) to http://pastebin.com"""
273 if last_exception is None:
274 print "SHELL: No Errors yet (Phew)..."
276 reason, event = last_exception.args
277 print "SHELL: Reason for the last error: '%s'" % reason
279 msg, filename = reason.split( ':' )
280 filename = filename.strip()
281 print "SHELL: Pasting log file to pastebin..."
283 status, error, location = sendToPastebin( open( filename ).read() )
286 print "SHELL: Pasted to %s" % location
288 print "ERROR: %s %s" % ( response.status, response.reason )
290 def parse( self, params ):
291 """(Re-)parse .bb files and calculate the dependency graph"""
292 cooker.status = cooker.ParsingStatus()
293 ignore = data.getVar("ASSUME_PROVIDED", make.cfg, 1) or ""
294 cooker.status.ignored_dependencies = set( ignore.split() )
295 cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", make.cfg, 1) )
297 make.collect_bbfiles( cooker.myProgressCallback )
298 cooker.buildDepgraph()
303 def print_( self, params ):
304 """Print the contents of an outer BitBake environment variable"""
306 value = data.getVar( var, make.cfg, 1 )
308 print_.usage = "<variable>"
310 def python( self, params ):
311 """Enter the expert mode - an interactive BitBake Python Interpreter"""
312 sys.ps1 = "EXPERT BB>>> "
313 sys.ps2 = "EXPERT BB... "
315 interpreter = code.InteractiveConsole( dict( globals() ) )
316 interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
318 def setVar( self, params ):
319 """Set an outer BitBake environment variable"""
321 data.setVar( var, value, make.cfg )
323 setVar.usage = "<variable> <value>"
325 def rebuild( self, params ):
326 """Clean and rebuild a .bb file or a providee"""
327 self.build( params, "clean" )
328 self.build( params, "build" )
329 rebuild.usage = "<providee>"
331 def shell( self, params ):
332 """Execute a shell command and dump the output"""
334 print commands.getoutput( " ".join( params ) )
335 shell.usage = "<...>"
337 def status( self, params ):
338 """<just for testing>"""
340 print "build cache = '%s'" % cooker.build_cache
341 print "build cache fail = '%s'" % cooker.build_cache_fail
342 print "building list = '%s'" % cooker.building_list
343 print "build path = '%s'" % cooker.build_path
344 print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
345 print "build stats = '%s'" % cooker.stats
346 if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
347 print "memory output contents = '%s'" % self._shell.myout._buffer
349 def test( self, params ):
350 """<just for testing>"""
351 print "testCommand called with '%s'" % params
353 def which( self, params ):
354 """Computes the providers for a given providee"""
358 print "SHELL: D'oh! The .bb files haven't been parsed yet. Next time call 'parse' before building stuff. This time I'll do it for 'ya."
361 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, make.cfg, 1 )
362 if not preferred: preferred = item
365 lv, lf, pv, pf = cooker.findBestProvider( preferred )
367 lv, lf, pv, pf = (None,)*4
370 providers = cooker.status.providers[item]
372 print "SHELL: ERROR: Nothing provides", preferred
374 for provider in providers:
375 if provider == pf: provider = " (***) %s" % provider
376 else: provider = " %s" % provider
378 which.usage = "<providee>"
380 ##########################################################################
381 # Common helper functions
382 ##########################################################################
384 def completeFilePath( bbfile ):
385 """Get the complete bbfile path"""
386 if not make.pkgdata: return bbfile
387 for key in make.pkgdata.keys():
388 if key.endswith( bbfile ):
392 def sendToPastebin( content ):
393 """Send content to http://www.pastebin.com"""
395 mydata["parent_pid"] = ""
396 mydata["format"] = "bash"
397 mydata["code2"] = content
398 mydata["paste"] = "Send"
399 mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
400 params = urllib.urlencode( mydata )
401 headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
403 conn = httplib.HTTPConnection( "pastebin.com:80" )
404 conn.request("POST", "/", params, headers )
406 response = conn.getresponse()
409 return response.status, response.reason, response.getheader( "location" ) or "unknown"
411 def completer( text, state ):
412 """Return a possible readline completion"""
413 if debug: print "(completer called with text='%s', state='%d'" % ( text, state )
416 line = readline.get_line_buffer()
419 # we are in second (or more) argument
420 if line[0] == "print" or line[0] == "set":
421 allmatches = make.cfg.keys()
422 elif line[0].startswith( "file" ):
423 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
424 else: allmatches = [ x.split("/")[-1] for x in make.pkgdata.keys() ]
425 elif line[0] == "build" or line[0] == "clean" or line[0] == "which":
426 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
427 else: allmatches = cooker.status.providers.iterkeys()
428 else: allmatches = [ "(No tab completion available for this command)" ]
430 # we are in first argument
431 allmatches = cmds.iterkeys()
433 completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
434 #print "completer.matches = '%s'" % completer.matches
435 if len( completer.matches ) > state:
436 return completer.matches[state]
441 ##########################################################################
443 ##########################################################################
446 """File-like output class buffering the output of the last 10 commands"""
447 def __init__( self, delegate ):
448 self.delegate = delegate
453 def startCommand( self, command ):
454 self._command = command
456 def endCommand( self ):
457 if self._command is not None:
458 if len( self._buffer ) == 10: del self._buffer[0]
459 self._buffer.append( ( self._command, self.text ) )
460 def removeLast( self ):
462 del self._buffer[ len( self._buffer ) - 1 ]
465 def bufferedCommands( self ):
466 return [ cmd for cmd, output in self._buffer ]
467 def buffer( self, i ):
468 if i < len( self._buffer ):
469 return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
470 else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
471 def write( self, text ):
472 if self._command is not None and text != "BB>> ": self.text.append( text )
473 self.delegate.write( text )
475 return self.delegate.flush()
477 return self.delegate.fileno()
479 return self.delegate.isatty()
481 ##########################################################################
483 ##########################################################################
487 def __init__( self ):
488 """Register commands and set up readline"""
489 self.commands = BitBakeShellCommands( self )
490 self.myout = MemoryOutput( sys.stdout )
492 readline.set_completer( completer )
493 readline.set_completer_delims( " " )
494 readline.parse_and_bind("tab: complete")
498 readline.read_history_file( history_file )
500 pass # It doesn't exist yet.
505 """Write readline history and clean up resources"""
506 if debug: print "(writing command history)"
509 readline.write_history_file( history_file )
511 print "SHELL: Unable to save command history"
513 def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
514 """Register a command"""
515 if usage == "": usage = command
516 if helptext == "": helptext = function.__doc__ or "<not yet documented>"
517 cmds[command] = ( function, numparams, usage, helptext )
519 def processCommand( self, command, params ):
520 """Process a command. Check number of params and print a usage string, if appropriate"""
521 if debug: print "(processing command '%s'...)" % command
523 function, numparams, usage, helptext = cmds[command]
525 print "SHELL: ERROR: '%s' command is not a valid command." % command
526 self.myout.removeLast()
528 if (numparams != -1) and (not len( params ) == numparams):
529 print "Usage: '%s'" % usage
532 result = function( self.commands, params )
533 if debug: print "(result was '%s')" % result
536 """The main command loop"""
537 while not leave_mainloop:
539 sys.stdout = self.myout.delegate
540 cmdline = raw_input( "BB>> " )
541 sys.stdout = self.myout
543 commands = cmdline.split( ';' )
544 for command in commands:
545 self.myout.startCommand( command )
547 self.processCommand( command.split()[0], command.split()[1:] )
549 self.processCommand( command, "" )
550 self.myout.endCommand()
554 except KeyboardInterrupt:
557 ##########################################################################
558 # Start function - called from the BitBake command line utility
559 ##########################################################################
561 def start( aCooker ):
564 bbshell = BitBakeShell()
568 if __name__ == "__main__":
569 print "SHELL: Sorry, this program should only be called by BitBake."