bitbake/lib/bb/utils.py:
[vuplus_bitbake] / lib / bb / shell.py
index f8176cd..b86dc97 100644 (file)
 # 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
+
 """
 
 ##########################################################################
@@ -43,11 +55,11 @@ try:
     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__
 
@@ -56,8 +68,8 @@ leave_mainloop = False
 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
@@ -70,13 +82,13 @@ 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 != "<...>":
@@ -85,6 +97,34 @@ class BitBakeShellCommands:
                     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]
@@ -104,24 +144,28 @@ class BitBakeShellCommands:
 
     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 ):
@@ -140,18 +184,22 @@ class BitBakeShellCommands:
     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
 
@@ -166,13 +214,18 @@ class BitBakeShellCommands:
         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:
@@ -185,7 +238,7 @@ class BitBakeShellCommands:
                 global last_exception
                 last_exception = e
 
-        make.options.cmd = oldcmd
+        cooker.configuration.cmd = oldcmd
     fileBuild.usage = "<bbfile>"
 
     def fileClean( self, params ):
@@ -193,16 +246,35 @@ class BitBakeShellCommands:
         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"""
@@ -230,10 +302,23 @@ class BitBakeShellCommands:
                 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 ):
@@ -285,7 +370,7 @@ SRC_URI = ""
         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 ):
@@ -305,7 +390,7 @@ SRC_URI = ""
                 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"""
@@ -315,22 +400,71 @@ SRC_URI = ""
     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"""
@@ -340,10 +474,15 @@ SRC_URI = ""
         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>"
 
@@ -389,11 +528,9 @@ SRC_URI = ""
         """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:
@@ -418,8 +555,8 @@ SRC_URI = ""
 
 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
@@ -445,7 +582,7 @@ def sendToPastebin( content ):
 
 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()
@@ -455,12 +592,12 @@ def completer( text, state ):
             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)" ]
@@ -475,6 +612,27 @@ def completer( text, state ):
     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
@@ -500,6 +658,9 @@ 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 ):
@@ -508,7 +669,7 @@ class MemoryOutput:
         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 ):
@@ -524,27 +685,32 @@ class BitBakeShell:
 
     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"
 
@@ -556,7 +722,7 @@ class BitBakeShell:
 
     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:
@@ -568,24 +734,58 @@ class BitBakeShell:
                 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
@@ -600,6 +800,7 @@ def start( aCooker ):
     global cooker
     cooker = aCooker
     bbshell = BitBakeShell()
+    bbshell.processStartupFile()
     bbshell.main()
     bbshell.cleanup()