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 ##########################################################################
23 # * Holger Freyther <zecke@handhelds.org>
24 # * Justin Patrin <papercrane@reversefold.com>
26 ##########################################################################
32 * list defined tasks per package
35 * command to reparse just one (or more) bbfile(s)
36 * automatic check if reparsing is necessary (inotify?)
37 * frontend for bb file manipulation
38 * more shell-like features:
39 - output control, i.e. pipe output into grep, sort, etc.
40 - job control, i.e. bring running commands into background and foreground
41 * start parsing in background right after startup
45 * force doesn't always work
46 * readline completion for commands with more than one parameters
50 ##########################################################################
51 # Import and setup global variables
52 ##########################################################################
57 from sets import Set as set
58 import sys, os, imp, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
59 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
60 from bb import data, parse, build, fatal
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__
67 leave_mainloop = False
72 debug = os.environ.get( "BBSHELL_DEBUG", "" )
74 ##########################################################################
75 # Class BitBakeShellCommands
76 ##########################################################################
78 class BitBakeShellCommands:
79 """This class contains the valid commands for the shell"""
81 def __init__( self, shell ):
82 """Register all the commands"""
84 for attr in BitBakeShellCommands.__dict__:
85 if not attr.startswith( "_" ):
86 if attr.endswith( "_" ):
87 command = attr[:-1].lower()
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", "" )
95 numArgs = len( usage.split() )
98 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
100 def _checkParsed( self ):
102 print "SHELL: This command needs to parse bbfiles..."
105 def _findProvider( self, item ):
107 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
108 if not preferred: preferred = item
110 lv, lf, pv, pf = cooker.findBestProvider( preferred )
112 if item in cooker.status.providers:
113 pf = cooker.status.providers[item][0]
118 def alias( self, params ):
119 """Register a new name for a command"""
122 print "ERROR: Command '%s' not known" % old
124 cmds[new] = cmds[old]
126 alias.usage = "<alias> <command>"
128 def buffer( self, params ):
129 """Dump specified output buffer"""
131 print self._shell.myout.buffer( int( index ) )
132 buffer.usage = "<index>"
134 def buffers( self, params ):
135 """Show the available output buffers"""
136 commands = self._shell.myout.bufferedCommands()
138 print "SHELL: No buffered commands available yet. Start doing something."
140 print "="*35, "Available Output Buffers", "="*27
141 for index, cmd in enumerate( commands ):
142 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
145 def build( self, params, cmd = "build" ):
146 """Build a providee"""
149 names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
150 if len( names ) == 0: names = [ globexpr ]
151 print "SHELL: Building %s" % ' '.join( names )
153 oldcmd = cooker.configuration.cmd
154 cooker.configuration.cmd = cmd
155 cooker.build_cache = []
156 cooker.build_cache_fail = []
160 cooker.buildProvider( name )
161 except build.EventException, e:
162 print "ERROR: Couldn't build '%s'" % name
163 global last_exception
167 cooker.configuration.cmd = oldcmd
169 build.usage = "<providee>"
171 def clean( self, params ):
172 """Clean a providee"""
173 self.build( params, "clean" )
174 clean.usage = "<providee>"
176 def compile( self, params ):
177 """Execute 'compile' on a providee"""
178 self.build( params, "compile" )
179 compile.usage = "<providee>"
181 def configure( self, params ):
182 """Execute 'configure' on a providee"""
183 self.build( params, "configure" )
184 configure.usage = "<providee>"
186 def edit( self, params ):
187 """Call $EDITOR on a providee"""
189 bbfile = self._findProvider( name )
190 if bbfile is not None:
191 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
193 print "ERROR: Nothing provides '%s'" % name
194 edit.usage = "<providee>"
196 def environment( self, params ):
197 """Dump out the outer BitBake environment (see bbread)"""
198 data.emit_env(sys.__stdout__, cooker.configuration.data, True)
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
206 def fetch( self, params ):
207 """Fetch a providee"""
208 self.build( params, "fetch" )
209 fetch.usage = "<providee>"
211 def fileBuild( self, params, cmd = "build" ):
212 """Parse and build a .bb file"""
214 bf = completeFilePath( name )
215 print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
217 oldcmd = cooker.configuration.cmd
218 cooker.configuration.cmd = cmd
219 cooker.build_cache = []
220 cooker.build_cache_fail = []
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
228 bbfile_data = parse.handle( bf, thisdata )
229 except parse.ParseError:
230 print "ERROR: Unable to open or parse '%s'" % bf
232 item = data.getVar('PN', bbfile_data, 1)
233 data.setVar( "_task_cache", [], bbfile_data ) # force
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
241 cooker.configuration.cmd = oldcmd
242 fileBuild.usage = "<bbfile>"
244 def fileClean( self, params ):
245 """Clean a .bb file"""
246 self.fileBuild( params, "clean" )
247 fileClean.usage = "<bbfile>"
249 def fileEdit( self, params ):
250 """Call $EDITOR on a .bb file"""
252 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
253 fileEdit.usage = "<bbfile>"
255 def fileRebuild( self, params ):
256 """Rebuild (clean & build) a .bb file"""
257 self.fileClean( params )
258 self.fileBuild( params )
259 fileRebuild.usage = "<bbfile>"
261 def fileReparse( self, params ):
262 """(re)Parse a bb file"""
264 print "SHELL: Parsing '%s'" % bbfile
265 parse.update_mtime( bbfile )
266 bb_data, fromCache = cooker.load_bbfile( bbfile )
267 cooker.pkgdata[bbfile] = bb_data
269 print "SHELL: File has not been updated, not reparsing"
271 print "SHELL: Parsed"
272 fileReparse.usage = "<bbfile>"
274 def force( self, params ):
275 """Toggle force task execution flag (see bitbake -f)"""
276 cooker.configuration.force = not cooker.configuration.force
277 print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
279 def help( self, params ):
280 """Show a comprehensive list of commands and their purpose"""
281 print "="*30, "Available Commands", "="*30
282 allcmds = cmds.keys()
285 function,numparams,usage,helptext = cmds[cmd]
286 print "| %s | %s" % (usage.ljust(30), helptext)
289 def lastError( self, params ):
290 """Show the reason or log that was produced by the last BitBake event exception"""
291 if last_exception is None:
292 print "SHELL: No Errors yet (Phew)..."
294 reason, event = last_exception.args
295 print "SHELL: Reason for the last error: '%s'" % reason
297 msg, filename = reason.split( ':' )
298 filename = filename.strip()
299 print "SHELL: Dumping log file for last error:"
301 print open( filename ).read()
303 print "ERROR: Couldn't open '%s'" % filename
305 def match( self, params ):
306 """Dump all files or providers matching a glob expression"""
307 what, globexpr = params
310 for key in globfilter( cooker.pkgdata.keys(), globexpr ): print key
311 elif what == "providers":
313 for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
315 print "Usage: match %s" % self.print_.usage
316 match.usage = "<files|providers> <glob>"
318 def new( self, params ):
319 """Create a new .bb file and open the editor"""
320 dirname, filename = params
321 packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
322 fulldirname = "%s/%s" % ( packages, dirname )
324 if not os.path.exists( fulldirname ):
325 print "SHELL: Creating '%s'" % fulldirname
326 os.mkdir( fulldirname )
327 if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
328 if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
329 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
331 print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
332 newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
333 print >>newpackage,"""DESCRIPTION = ""
362 os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
363 new.usage = "<directory> <filename>"
365 def pasteBin( self, params ):
366 """Send a command + output buffer to http://pastebin.com"""
368 contents = self._shell.myout.buffer( int( index ) )
369 status, error, location = sendToPastebin( contents )
371 print "SHELL: Pasted to %s" % location
373 print "ERROR: %s %s" % ( status, error )
374 pasteBin.usage = "<index>"
376 def pasteLog( self, params ):
377 """Send the last event exception error log (if there is one) to http://pastebin.com"""
378 if last_exception is None:
379 print "SHELL: No Errors yet (Phew)..."
381 reason, event = last_exception.args
382 print "SHELL: Reason for the last error: '%s'" % reason
384 msg, filename = reason.split( ':' )
385 filename = filename.strip()
386 print "SHELL: Pasting log file to pastebin..."
388 status, error, location = sendToPastebin( open( filename ).read() )
391 print "SHELL: Pasted to %s" % location
393 print "ERROR: %s %s" % ( status, error )
395 def patch( self, params ):
396 """Execute 'patch' command on a providee"""
397 self.build( params, "patch" )
398 patch.usage = "<providee>"
400 def parse( self, params ):
401 """(Re-)parse .bb files and calculate the dependency graph"""
402 cooker.status = cooker.ParsingStatus()
403 ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
404 cooker.status.ignored_dependencies = set( ignore.split() )
405 cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
407 cooker.collect_bbfiles( cooker.myProgressCallback )
408 cooker.buildDepgraph()
413 def reparse( self, params ):
414 """(re)Parse a providee's bb file"""
415 bbfile = self._findProvider( params[0] )
416 if bbfile is not None:
417 print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
418 self.fileReparse( [ bbfile ] )
420 print "ERROR: Nothing provides '%s'" % params[0]
421 reparse.usage = "<providee>"
423 def getvar( self, params ):
424 """Dump the contents of an outer BitBake environment variable"""
426 value = data.getVar( var, cooker.configuration.data, 1 )
428 getvar.usage = "<variable>"
430 def peek( self, params ):
431 """Dump contents of variable defined in providee's metadata"""
433 bbfile = self._findProvider( name )
434 if bbfile is not None:
435 value = cooker.pkgdata[bbfile].getVar( var, 1 )
438 print "ERROR: Nothing provides '%s'" % name
439 peek.usage = "<providee> <variable>"
441 def poke( self, params ):
442 """Set contents of variable defined in providee's metadata"""
443 name, var, value = params
444 bbfile = self._findProvider( name )
445 d = cooker.pkgdata[bbfile]
446 if bbfile is not None:
447 data.setVar( var, value, d )
449 # mark the change semi persistant
450 cooker.pkgdata.setDirty(bbfile, d)
453 print "ERROR: Nothing provides '%s'" % name
454 poke.usage = "<providee> <variable> <value>"
456 def print_( self, params ):
457 """Dump all files or providers"""
461 for key in cooker.pkgdata.keys(): print key
462 elif what == "providers":
464 for key in cooker.status.providers.keys(): print key
466 print "Usage: print %s" % self.print_.usage
467 print_.usage = "<files|providers>"
469 def python( self, params ):
470 """Enter the expert mode - an interactive BitBake Python Interpreter"""
471 sys.ps1 = "EXPERT BB>>> "
472 sys.ps2 = "EXPERT BB... "
474 interpreter = code.InteractiveConsole( dict( globals() ) )
475 interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
477 def showdata( self, params ):
478 """Execute 'showdata' on a providee"""
479 self.build( params, "showdata" )
480 showdata.usage = "<providee>"
482 def setVar( self, params ):
483 """Set an outer BitBake environment variable"""
485 data.setVar( var, value, cooker.configuration.data )
487 setVar.usage = "<variable> <value>"
489 def rebuild( self, params ):
490 """Clean and rebuild a .bb file or a providee"""
491 self.build( params, "clean" )
492 self.build( params, "build" )
493 rebuild.usage = "<providee>"
495 def shell( self, params ):
496 """Execute a shell command and dump the output"""
498 print commands.getoutput( " ".join( params ) )
499 shell.usage = "<...>"
501 def stage( self, params ):
502 """Execute 'stage' on a providee"""
503 self.build( params, "stage" )
504 stage.usage = "<providee>"
506 def status( self, params ):
507 """<just for testing>"""
509 print "build cache = '%s'" % cooker.build_cache
510 print "build cache fail = '%s'" % cooker.build_cache_fail
511 print "building list = '%s'" % cooker.building_list
512 print "build path = '%s'" % cooker.build_path
513 print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
514 print "build stats = '%s'" % cooker.stats
515 if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
516 print "memory output contents = '%s'" % self._shell.myout._buffer
518 def test( self, params ):
519 """<just for testing>"""
520 print "testCommand called with '%s'" % params
522 def unpack( self, params ):
523 """Execute 'unpack' on a providee"""
524 self.build( params, "unpack" )
525 unpack.usage = "<providee>"
527 def which( self, params ):
528 """Computes the providers for a given providee"""
533 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
534 if not preferred: preferred = item
537 lv, lf, pv, pf = cooker.findBestProvider( preferred )
539 lv, lf, pv, pf = (None,)*4
542 providers = cooker.status.providers[item]
544 print "SHELL: ERROR: Nothing provides", preferred
546 for provider in providers:
547 if provider == pf: provider = " (***) %s" % provider
548 else: provider = " %s" % provider
550 which.usage = "<providee>"
552 ##########################################################################
553 # Common helper functions
554 ##########################################################################
556 def completeFilePath( bbfile ):
557 """Get the complete bbfile path"""
558 if not cooker.pkgdata: return bbfile
559 for key in cooker.pkgdata.keys():
560 if key.endswith( bbfile ):
564 def sendToPastebin( content ):
565 """Send content to http://www.pastebin.com"""
567 mydata["parent_pid"] = ""
568 mydata["format"] = "bash"
569 mydata["code2"] = content
570 mydata["paste"] = "Send"
571 mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
572 params = urllib.urlencode( mydata )
573 headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
575 conn = httplib.HTTPConnection( "pastebin.com:80" )
576 conn.request("POST", "/", params, headers )
578 response = conn.getresponse()
581 return response.status, response.reason, response.getheader( "location" ) or "unknown"
583 def completer( text, state ):
584 """Return a possible readline completion"""
585 debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
588 line = readline.get_line_buffer()
591 # we are in second (or more) argument
592 if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
593 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
594 if u == "<variable>":
595 allmatches = cooker.configuration.data.keys()
596 elif u == "<bbfile>":
597 if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
598 else: allmatches = [ x.split("/")[-1] for x in cooker.pkgdata.keys() ]
599 elif u == "<providee>":
600 if cooker.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
601 else: allmatches = cooker.status.providers.iterkeys()
602 else: allmatches = [ "(No tab completion available for this command)" ]
603 else: allmatches = [ "(No tab completion available for this command)" ]
605 # we are in first argument
606 allmatches = cmds.iterkeys()
608 completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
609 #print "completer.matches = '%s'" % completer.matches
610 if len( completer.matches ) > state:
611 return completer.matches[state]
615 def debugOut( text ):
617 sys.stderr.write( "( %s )\n" % text )
619 def columnize( alist, width = 80 ):
621 A word-wrap function that preserves existing line breaks
622 and most spaces in the text. Expects that existing line
623 breaks are posix newlines (\n).
625 return reduce(lambda line, word, width=width: '%s%s%s' %
627 ' \n'[(len(line[line.rfind('\n')+1:])
628 + len(word.split('\n',1)[0]
634 def globfilter( names, pattern ):
635 return fnmatch.filter( names, pattern )
637 ##########################################################################
639 ##########################################################################
642 """File-like output class buffering the output of the last 10 commands"""
643 def __init__( self, delegate ):
644 self.delegate = delegate
649 def startCommand( self, command ):
650 self._command = command
652 def endCommand( self ):
653 if self._command is not None:
654 if len( self._buffer ) == 10: del self._buffer[0]
655 self._buffer.append( ( self._command, self.text ) )
656 def removeLast( self ):
658 del self._buffer[ len( self._buffer ) - 1 ]
661 def lastBuffer( self ):
663 return self._buffer[ len( self._buffer ) -1 ][1]
664 def bufferedCommands( self ):
665 return [ cmd for cmd, output in self._buffer ]
666 def buffer( self, i ):
667 if i < len( self._buffer ):
668 return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
669 else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
670 def write( self, text ):
671 if self._command is not None and text != "BB>> ": self.text.append( text )
672 if self.delegate is not None: self.delegate.write( text )
674 return self.delegate.flush()
676 return self.delegate.fileno()
678 return self.delegate.isatty()
680 ##########################################################################
682 ##########################################################################
686 def __init__( self ):
687 """Register commands and set up readline"""
688 self.commandQ = Queue.Queue()
689 self.commands = BitBakeShellCommands( self )
690 self.myout = MemoryOutput( sys.stdout )
691 self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
692 self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
694 readline.set_completer( completer )
695 readline.set_completer_delims( " " )
696 readline.parse_and_bind("tab: complete")
699 readline.read_history_file( self.historyfilename )
701 pass # It doesn't exist yet.
705 # save initial cooker configuration (will be reused in file*** commands)
707 initdata = copy.deepcopy( cooker.configuration.data )
710 """Write readline history and clean up resources"""
711 debugOut( "writing command history" )
713 readline.write_history_file( self.historyfilename )
715 print "SHELL: Unable to save command history"
717 def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
718 """Register a command"""
719 if usage == "": usage = command
720 if helptext == "": helptext = function.__doc__ or "<not yet documented>"
721 cmds[command] = ( function, numparams, usage, helptext )
723 def processCommand( self, command, params ):
724 """Process a command. Check number of params and print a usage string, if appropriate"""
725 debugOut( "processing command '%s'..." % command )
727 function, numparams, usage, helptext = cmds[command]
729 print "SHELL: ERROR: '%s' command is not a valid command." % command
730 self.myout.removeLast()
732 if (numparams != -1) and (not len( params ) == numparams):
733 print "Usage: '%s'" % usage
736 result = function( self.commands, params )
737 debugOut( "result was '%s'" % result )
739 def processStartupFile( self ):
740 """Read and execute all commands found in $HOME/.bbsh_startup"""
741 if os.path.exists( self.startupfilename ):
742 startupfile = open( self.startupfilename, "r" )
743 for cmdline in startupfile:
744 debugOut( "processing startup line '%s'" % cmdline )
748 print "ERROR: '|' in startup file is not allowed. Ignoring line"
750 self.commandQ.put( cmdline.strip() )
753 """The main command loop"""
754 while not leave_mainloop:
756 if self.commandQ.empty():
757 sys.stdout = self.myout.delegate
758 cmdline = raw_input( "BB>> " )
759 sys.stdout = self.myout
761 cmdline = self.commandQ.get()
763 allCommands = cmdline.split( ';' )
764 for command in allCommands:
767 # special case for expert mode
768 if command == 'python':
769 sys.stdout = self.myout.delegate
770 self.processCommand( command, "" )
771 sys.stdout = self.myout
773 self.myout.startCommand( command )
774 if '|' in command: # disable output
775 command, pipecmd = command.split( '|' )
776 delegate = self.myout.delegate
777 self.myout.delegate = None
778 tokens = shlex.split( command, True )
779 self.processCommand( tokens[0], tokens[1:] or "" )
780 self.myout.endCommand()
781 if pipecmd is not None: # restore output
782 self.myout.delegate = delegate
784 pipe = popen2.Popen4( pipecmd )
785 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
787 sys.stdout.write( pipe.fromchild.read() )
792 except KeyboardInterrupt:
795 ##########################################################################
796 # Start function - called from the BitBake command line utility
797 ##########################################################################
799 def start( aCooker ):
802 bbshell = BitBakeShell()
803 bbshell.processStartupFile()
807 if __name__ == "__main__":
808 print "SHELL: Sorry, this program should only be called by BitBake."