# Place, Suite 330, Boston, MA 02111-1307 USA.
#
##########################################################################
+#
+# Thanks to:
+# * Holger Freyther <zecke@handhelds.org>
+# * Justin Patrin <papercrane@reversefold.com>
+#
+##########################################################################
"""
BitBake Shell
-TODO:
- * specify tasks
- * specify force
+IDEAS:
+ * list defined tasks per package
+ * list classes
+ * toggle force
* command to reparse just one (or more) bbfile(s)
* automatic check if reparsing is necessary (inotify?)
- * frontend for bb file manipulation?
- * pipe output of commands into shell commands (i.e grep or sort)?
- * job control, i.e. bring commands into background with '&', fg, bg, etc.?
- * start parsing in background right after startup?
- * print variable from package data
- * command aliases / shortcuts?
+ * frontend for bb file manipulation
+ * more shell-like features:
+ - output control, i.e. pipe output into grep, sort, etc.
+ - job control, i.e. bring running commands into background and foreground
+ * start parsing in background right after startup
+ * ncurses interface
+
+PROBLEMS:
+ * force doesn't always work
+ * readline completion for commands with more than one parameters
+
"""
##########################################################################
set
except NameError:
from sets import Set as set
-import sys, os, imp, readline, socket, httplib, urllib, commands
+import sys, os, imp, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
-from bb import data, parse, build, make, fatal
+from bb import data, parse, build, fatal
-__version__ = "0.5.0"
+__version__ = "0.5.3"
__credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
Type 'help' for more information, press CTRL-D to exit.""" % __version__
last_exception = None
cooker = None
parsed = False
-debug = os.environ.get( "BBSHELL_DEBUG", "" ) != ""
-history_file = "%s/.bbsh_history" % os.environ.get( "HOME" )
+initdata = None
+debug = os.environ.get( "BBSHELL_DEBUG", "" )
##########################################################################
# Class BitBakeShellCommands
"""Register all the commands"""
self._shell = shell
for attr in BitBakeShellCommands.__dict__:
- if not attr.startswith( "__" ):
+ if not attr.startswith( "_" ):
if attr.endswith( "_" ):
command = attr[:-1].lower()
else:
command = attr[:].lower()
method = getattr( BitBakeShellCommands, attr )
- if debug: print "registering command '%s'" % command
+ debugOut( "registering command '%s'" % command )
# scan number of arguments
usage = getattr( method, "usage", "" )
if usage != "<...>":
numArgs = -1
shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
+ def _checkParsed( self ):
+ if not parsed:
+ print "SHELL: This command needs to parse bbfiles..."
+ self.parse( None )
+
+ def _findProvider( self, item ):
+ self._checkParsed()
+ preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
+ if not preferred: preferred = item
+ try:
+ lv, lf, pv, pf = cooker.findBestProvider( preferred )
+ except KeyError:
+ if item in cooker.status.providers:
+ pf = cooker.status.providers[item][0]
+ else:
+ pf = None
+ return pf
+
+ def alias( self, params ):
+ """Register a new name for a command"""
+ new, old = params
+ if not old in cmds:
+ print "ERROR: Command '%s' not known" % old
+ else:
+ cmds[new] = cmds[old]
+ print "OK"
+ alias.usage = "<alias> <command>"
+
def buffer( self, params ):
"""Dump specified output buffer"""
index = params[0]
def build( self, params, cmd = "build" ):
"""Build a providee"""
- name = params[0]
-
- oldcmd = make.options.cmd
- make.options.cmd = cmd
+ globexpr = params[0]
+ self._checkParsed()
+ names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
+ if len( names ) == 0: names = [ globexpr ]
+ print "SHELL: Building %s" % ' '.join( names )
+
+ oldcmd = cooker.configuration.cmd
+ cooker.configuration.cmd = cmd
cooker.build_cache = []
cooker.build_cache_fail = []
- if not parsed:
- 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."
- self.parse( None )
- try:
- cooker.buildProvider( name )
- except build.EventException, e:
- print "ERROR: Couldn't build '%s'" % name
- global last_exception
- last_exception = e
+ for name in names:
+ try:
+ cooker.buildProvider( name, data.getVar("BUILD_ALL_DEPS", cooker.configuration.data, True) )
+ except build.EventException, e:
+ print "ERROR: Couldn't build '%s'" % name
+ global last_exception
+ last_exception = e
+ break
+
+ cooker.configuration.cmd = oldcmd
- make.options.cmd = oldcmd
build.usage = "<providee>"
def clean( self, params ):
configure.usage = "<providee>"
def edit( self, params ):
- """Call $EDITOR on a .bb file"""
+ """Call $EDITOR on a providee"""
name = params[0]
- os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
- edit.usage = "<bbfile>"
+ bbfile = self._findProvider( name )
+ if bbfile is not None:
+ os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
+ else:
+ print "ERROR: Nothing provides '%s'" % name
+ edit.usage = "<providee>"
def environment( self, params ):
"""Dump out the outer BitBake environment (see bbread)"""
- data.emit_env(sys.__stdout__, make.cfg, True)
+ data.emit_env(sys.__stdout__, cooker.configuration.data, True)
def exit_( self, params ):
"""Leave the BitBake Shell"""
- if debug: print "(setting leave_mainloop to true)"
+ debugOut( "setting leave_mainloop to true" )
global leave_mainloop
leave_mainloop = True
bf = completeFilePath( name )
print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
- oldcmd = make.options.cmd
- make.options.cmd = cmd
+ oldcmd = cooker.configuration.cmd
+ cooker.configuration.cmd = cmd
cooker.build_cache = []
cooker.build_cache_fail = []
+ thisdata = copy.deepcopy( initdata )
+ # Caution: parse.handle modifies thisdata, hence it would
+ # lead to pollution cooker.configuration.data, which is
+ # why we use it on a safe copy we obtained from cooker right after
+ # parsing the initial *.conf files
try:
- bbfile_data = parse.handle( bf, make.cfg )
+ bbfile_data = parse.handle( bf, thisdata )
except parse.ParseError:
print "ERROR: Unable to open or parse '%s'" % bf
else:
global last_exception
last_exception = e
- make.options.cmd = oldcmd
+ cooker.configuration.cmd = oldcmd
fileBuild.usage = "<bbfile>"
def fileClean( self, params ):
self.fileBuild( params, "clean" )
fileClean.usage = "<bbfile>"
+ def fileEdit( self, params ):
+ """Call $EDITOR on a .bb file"""
+ name = params[0]
+ os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
+ fileEdit.usage = "<bbfile>"
+
def fileRebuild( self, params ):
"""Rebuild (clean & build) a .bb file"""
self.fileClean( params )
self.fileBuild( params )
fileRebuild.usage = "<bbfile>"
+ def fileReparse( self, params ):
+ """(re)Parse a bb file"""
+ bbfile = params[0]
+ print "SHELL: Parsing '%s'" % bbfile
+ parse.update_mtime( bbfile )
+ bb_data, fromCache = cooker.load_bbfile( bbfile )
+ cooker.pkgdata[bbfile] = bb_data
+ if fromCache:
+ print "SHELL: File has not been updated, not reparsing"
+ else:
+ print "SHELL: Parsed"
+ fileReparse.usage = "<bbfile>"
+
def force( self, params ):
"""Toggle force task execution flag (see bitbake -f)"""
- make.options.force = not make.options.force
- print "SHELL: Force Flag is now '%s'" % repr( make.options.force )
+ cooker.configuration.force = not cooker.configuration.force
+ print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
def help( self, params ):
"""Show a comprehensive list of commands and their purpose"""
except IOError:
print "ERROR: Couldn't open '%s'" % filename
+ def match( self, params ):
+ """Dump all files or providers matching a glob expression"""
+ what, globexpr = params
+ if what == "files":
+ self._checkParsed()
+ for key in globfilter( cooker.pkgdata.keys(), globexpr ): print key
+ elif what == "providers":
+ self._checkParsed()
+ for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
+ else:
+ print "Usage: match %s" % self.print_.usage
+ match.usage = "<files|providers> <glob>"
+
def new( self, params ):
"""Create a new .bb file and open the editor"""
dirname, filename = params
- packages = '/'.join( data.getVar( "BBFILES", make.cfg, 1 ).split('/')[:-2] )
+ packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
fulldirname = "%s/%s" % ( packages, dirname )
if not os.path.exists( fulldirname ):
if status == 302:
print "SHELL: Pasted to %s" % location
else:
- print "ERROR: %s %s" % ( response.status, response.reason )
+ print "ERROR: %s %s" % ( status, error )
pasteBin.usage = "<index>"
def pasteLog( self, params ):
if status == 302:
print "SHELL: Pasted to %s" % location
else:
- print "ERROR: %s %s" % ( response.status, response.reason )
+ print "ERROR: %s %s" % ( status, error )
def patch( self, params ):
"""Execute 'patch' command on a providee"""
def parse( self, params ):
"""(Re-)parse .bb files and calculate the dependency graph"""
cooker.status = cooker.ParsingStatus()
- ignore = data.getVar("ASSUME_PROVIDED", make.cfg, 1) or ""
+ ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
cooker.status.ignored_dependencies = set( ignore.split() )
- cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", make.cfg, 1) )
+ cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
- make.collect_bbfiles( cooker.myProgressCallback )
+ cooker.collect_bbfiles( cooker.myProgressCallback )
cooker.buildDepgraph()
global parsed
parsed = True
print
- def print_( self, params ):
- """Print the contents of an outer BitBake environment variable"""
+ def reparse( self, params ):
+ """(re)Parse a providee's bb file"""
+ bbfile = self._findProvider( params[0] )
+ if bbfile is not None:
+ print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
+ self.fileReparse( [ bbfile ] )
+ else:
+ print "ERROR: Nothing provides '%s'" % params[0]
+ reparse.usage = "<providee>"
+
+ def getvar( self, params ):
+ """Dump the contents of an outer BitBake environment variable"""
var = params[0]
- value = data.getVar( var, make.cfg, 1 )
+ value = data.getVar( var, cooker.configuration.data, 1 )
print value
- print_.usage = "<variable>"
+ getvar.usage = "<variable>"
+
+ def peek( self, params ):
+ """Dump contents of variable defined in providee's metadata"""
+ name, var = params
+ bbfile = self._findProvider( name )
+ if bbfile is not None:
+ value = cooker.pkgdata[bbfile].getVar( var, 1 )
+ print value
+ else:
+ print "ERROR: Nothing provides '%s'" % name
+ peek.usage = "<providee> <variable>"
+
+ def poke( self, params ):
+ """Set contents of variable defined in providee's metadata"""
+ name, var, value = params
+ bbfile = self._findProvider( name )
+ d = cooker.pkgdata[bbfile]
+ if bbfile is not None:
+ data.setVar( var, value, d )
+
+ # mark the change semi persistant
+ cooker.pkgdata.setDirty(bbfile, d)
+ print "OK"
+ else:
+ print "ERROR: Nothing provides '%s'" % name
+ poke.usage = "<providee> <variable> <value>"
+
+ def print_( self, params ):
+ """Dump all files or providers"""
+ what = params[0]
+ if what == "files":
+ self._checkParsed()
+ for key in cooker.pkgdata.keys(): print key
+ elif what == "providers":
+ self._checkParsed()
+ for key in cooker.status.providers.keys(): print key
+ else:
+ print "Usage: print %s" % self.print_.usage
+ print_.usage = "<files|providers>"
def python( self, params ):
"""Enter the expert mode - an interactive BitBake Python Interpreter"""
interpreter = code.InteractiveConsole( dict( globals() ) )
interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
+ def showdata( self, params ):
+ """Execute 'showdata' on a providee"""
+ self.build( params, "showdata" )
+ showdata.usage = "<providee>"
+
def setVar( self, params ):
"""Set an outer BitBake environment variable"""
var, value = params
- data.setVar( var, value, make.cfg )
+ data.setVar( var, value, cooker.configuration.data )
print "OK"
setVar.usage = "<variable> <value>"
"""Computes the providers for a given providee"""
item = params[0]
- if not parsed:
- 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."
- self.parse( None )
+ self._checkParsed()
- preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, make.cfg, 1 )
+ preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
if not preferred: preferred = item
try:
def completeFilePath( bbfile ):
"""Get the complete bbfile path"""
- if not make.pkgdata: return bbfile
- for key in make.pkgdata.keys():
+ if not cooker.pkgdata: return bbfile
+ for key in cooker.pkgdata.keys():
if key.endswith( bbfile ):
return key
return bbfile
def completer( text, state ):
"""Return a possible readline completion"""
- if debug: print "(completer called with text='%s', state='%d'" % ( text, state )
+ debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
if state == 0:
line = readline.get_line_buffer()
if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
u = getattr( cmds[line[0]][0], "usage" ).split()[0]
if u == "<variable>":
- allmatches = make.cfg.keys()
+ allmatches = cooker.configuration.data.keys()
elif u == "<bbfile>":
- if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
- else: allmatches = [ x.split("/")[-1] for x in make.pkgdata.keys() ]
+ if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
+ else: allmatches = [ x.split("/")[-1] for x in cooker.pkgdata.keys() ]
elif u == "<providee>":
- if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
+ if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
else: allmatches = cooker.status.providers.iterkeys()
else: allmatches = [ "(No tab completion available for this command)" ]
else: allmatches = [ "(No tab completion available for this command)" ]
else:
return None
+def debugOut( text ):
+ if debug:
+ sys.stderr.write( "( %s )\n" % text )
+
+def columnize( alist, width = 80 ):
+ """
+ A word-wrap function that preserves existing line breaks
+ and most spaces in the text. Expects that existing line
+ breaks are posix newlines (\n).
+ """
+ return reduce(lambda line, word, width=width: '%s%s%s' %
+ (line,
+ ' \n'[(len(line[line.rfind('\n')+1:])
+ + len(word.split('\n',1)[0]
+ ) >= width)],
+ word),
+ alist
+ )
+
+def globfilter( names, pattern ):
+ return fnmatch.filter( names, pattern )
##########################################################################
# Class MemoryOutput
del self._buffer[ len( self._buffer ) - 1 ]
self.text = []
self._command = None
+ def lastBuffer( self ):
+ if self._buffer:
+ return self._buffer[ len( self._buffer ) -1 ][1]
def bufferedCommands( self ):
return [ cmd for cmd, output in self._buffer ]
def buffer( self, i ):
else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
def write( self, text ):
if self._command is not None and text != "BB>> ": self.text.append( text )
- self.delegate.write( text )
+ if self.delegate is not None: self.delegate.write( text )
def flush( self ):
return self.delegate.flush()
def fileno( self ):
def __init__( self ):
"""Register commands and set up readline"""
+ self.commandQ = Queue.Queue()
self.commands = BitBakeShellCommands( self )
self.myout = MemoryOutput( sys.stdout )
+ self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
+ self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
readline.set_completer( completer )
readline.set_completer_delims( " " )
readline.parse_and_bind("tab: complete")
try:
- global history_file
- readline.read_history_file( history_file )
+ readline.read_history_file( self.historyfilename )
except IOError:
pass # It doesn't exist yet.
print __credits__
+ # save initial cooker configuration (will be reused in file*** commands)
+ global initdata
+ initdata = copy.deepcopy( cooker.configuration.data )
+
def cleanup( self ):
"""Write readline history and clean up resources"""
- if debug: print "(writing command history)"
+ debugOut( "writing command history" )
try:
- global history_file
- readline.write_history_file( history_file )
+ readline.write_history_file( self.historyfilename )
except:
print "SHELL: Unable to save command history"
def processCommand( self, command, params ):
"""Process a command. Check number of params and print a usage string, if appropriate"""
- if debug: print "(processing command '%s'...)" % command
+ debugOut( "processing command '%s'..." % command )
try:
function, numparams, usage, helptext = cmds[command]
except KeyError:
return
result = function( self.commands, params )
- if debug: print "(result was '%s')" % result
+ debugOut( "result was '%s'" % result )
+
+ def processStartupFile( self ):
+ """Read and execute all commands found in $HOME/.bbsh_startup"""
+ if os.path.exists( self.startupfilename ):
+ startupfile = open( self.startupfilename, "r" )
+ for cmdline in startupfile:
+ debugOut( "processing startup line '%s'" % cmdline )
+ if not cmdline:
+ continue
+ if "|" in cmdline:
+ print "ERROR: '|' in startup file is not allowed. Ignoring line"
+ continue
+ self.commandQ.put( cmdline.strip() )
def main( self ):
"""The main command loop"""
while not leave_mainloop:
try:
- sys.stdout = self.myout.delegate
- cmdline = raw_input( "BB>> " )
- sys.stdout = self.myout
+ if self.commandQ.empty():
+ sys.stdout = self.myout.delegate
+ cmdline = raw_input( "BB>> " )
+ sys.stdout = self.myout
+ else:
+ cmdline = self.commandQ.get()
if cmdline:
- commands = cmdline.split( ';' )
- for command in commands:
- self.myout.startCommand( command )
- if ' ' in command:
- self.processCommand( command.split()[0], command.split()[1:] )
- else:
+ allCommands = cmdline.split( ';' )
+ for command in allCommands:
+ pipecmd = None
+ #
+ # special case for expert mode
+ if command == 'python':
+ sys.stdout = self.myout.delegate
self.processCommand( command, "" )
- self.myout.endCommand()
+ sys.stdout = self.myout
+ else:
+ self.myout.startCommand( command )
+ if '|' in command: # disable output
+ command, pipecmd = command.split( '|' )
+ delegate = self.myout.delegate
+ self.myout.delegate = None
+ tokens = shlex.split( command, True )
+ self.processCommand( tokens[0], tokens[1:] or "" )
+ self.myout.endCommand()
+ if pipecmd is not None: # restore output
+ self.myout.delegate = delegate
+
+ pipe = popen2.Popen4( pipecmd )
+ pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
+ pipe.tochild.close()
+ sys.stdout.write( pipe.fromchild.read() )
+ #
except EOFError:
print
return
global cooker
cooker = aCooker
bbshell = BitBakeShell()
+ bbshell.processStartupFile()
bbshell.main()
bbshell.cleanup()