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-2006 Michael 'Mickey' Lauer <mickey@Vanille.de>
7 # Copyright (C) 2005-2006 Vanille Media
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.
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.
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.
21 ##########################################################################
24 # * Holger Freyther <zecke@handhelds.org>
25 # * Justin Patrin <papercrane@reversefold.com>
27 ##########################################################################
33 * list defined tasks per package
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
46 * force doesn't always work
47 * readline completion for commands with more than one parameters
51 ##########################################################################
52 # Import and setup global variables
53 ##########################################################################
58 from sets import Set as set
59 import sys, os, imp, readline, socket, httplib, urllib, commands, popen2, copy, shlex, Queue, fnmatch
60 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
61 from bb import data, parse, build, fatal
63 __version__ = "0.5.3.1"
64 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
65 Type 'help' for more information, press CTRL-D to exit.""" % __version__
68 leave_mainloop = False
73 debug = os.environ.get( "BBSHELL_DEBUG", "" )
75 ##########################################################################
76 # Class BitBakeShellCommands
77 ##########################################################################
79 class BitBakeShellCommands:
80 """This class contains the valid commands for the shell"""
82 def __init__( self, shell ):
83 """Register all the commands"""
85 for attr in BitBakeShellCommands.__dict__:
86 if not attr.startswith( "_" ):
87 if attr.endswith( "_" ):
88 command = attr[:-1].lower()
90 command = attr[:].lower()
91 method = getattr( BitBakeShellCommands, attr )
92 debugOut( "registering command '%s'" % command )
93 # scan number of arguments
94 usage = getattr( method, "usage", "" )
96 numArgs = len( usage.split() )
99 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
101 def _checkParsed( self ):
103 print "SHELL: This command needs to parse bbfiles..."
106 def _findProvider( self, item ):
108 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
109 if not preferred: preferred = item
111 lv, lf, pv, pf = cooker.findBestProvider( preferred )
113 if item in cooker.status.providers:
114 pf = cooker.status.providers[item][0]
119 def alias( self, params ):
120 """Register a new name for a command"""
123 print "ERROR: Command '%s' not known" % old
125 cmds[new] = cmds[old]
127 alias.usage = "<alias> <command>"
129 def buffer( self, params ):
130 """Dump specified output buffer"""
132 print self._shell.myout.buffer( int( index ) )
133 buffer.usage = "<index>"
135 def buffers( self, params ):
136 """Show the available output buffers"""
137 commands = self._shell.myout.bufferedCommands()
139 print "SHELL: No buffered commands available yet. Start doing something."
141 print "="*35, "Available Output Buffers", "="*27
142 for index, cmd in enumerate( commands ):
143 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
146 def build( self, params, cmd = "build" ):
147 """Build a providee"""
150 names = globfilter( cooker.status.pkg_pn.keys(), globexpr )
151 if len( names ) == 0: names = [ globexpr ]
152 print "SHELL: Building %s" % ' '.join( names )
154 oldcmd = cooker.configuration.cmd
155 cooker.configuration.cmd = cmd
156 cooker.build_cache = []
157 cooker.build_cache_fail = []
161 cooker.buildProvider( name, data.getVar("BUILD_ALL_DEPS", cooker.configuration.data, True) )
162 except build.EventException, e:
163 print "ERROR: Couldn't build '%s'" % name
164 global last_exception
168 cooker.configuration.cmd = oldcmd
170 build.usage = "<providee>"
172 def clean( self, params ):
173 """Clean a providee"""
174 self.build( params, "clean" )
175 clean.usage = "<providee>"
177 def compile( self, params ):
178 """Execute 'compile' on a providee"""
179 self.build( params, "compile" )
180 compile.usage = "<providee>"
182 def configure( self, params ):
183 """Execute 'configure' on a providee"""
184 self.build( params, "configure" )
185 configure.usage = "<providee>"
187 def edit( self, params ):
188 """Call $EDITOR on a providee"""
190 bbfile = self._findProvider( name )
191 if bbfile is not None:
192 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
194 print "ERROR: Nothing provides '%s'" % name
195 edit.usage = "<providee>"
197 def environment( self, params ):
198 """Dump out the outer BitBake environment (see bbread)"""
199 data.emit_env(sys.__stdout__, cooker.configuration.data, True)
201 def exit_( self, params ):
202 """Leave the BitBake Shell"""
203 debugOut( "setting leave_mainloop to true" )
204 global leave_mainloop
205 leave_mainloop = True
207 def fetch( self, params ):
208 """Fetch a providee"""
209 self.build( params, "fetch" )
210 fetch.usage = "<providee>"
212 def fileBuild( self, params, cmd = "build" ):
213 """Parse and build a .bb file"""
215 bf = completeFilePath( name )
216 print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
218 oldcmd = cooker.configuration.cmd
219 cooker.configuration.cmd = cmd
220 cooker.build_cache = []
221 cooker.build_cache_fail = []
223 thisdata = copy.deepcopy( initdata )
224 # Caution: parse.handle modifies thisdata, hence it would
225 # lead to pollution cooker.configuration.data, which is
226 # why we use it on a safe copy we obtained from cooker right after
227 # parsing the initial *.conf files
229 bbfile_data = parse.handle( bf, thisdata )
230 except parse.ParseError:
231 print "ERROR: Unable to open or parse '%s'" % bf
233 item = data.getVar('PN', bbfile_data, 1)
234 data.setVar( "_task_cache", [], bbfile_data ) # force
236 cooker.tryBuildPackage( os.path.abspath( bf ), item, bbfile_data )
237 except build.EventException, e:
238 print "ERROR: Couldn't build '%s'" % name
239 global last_exception
242 cooker.configuration.cmd = oldcmd
243 fileBuild.usage = "<bbfile>"
245 def fileClean( self, params ):
246 """Clean a .bb file"""
247 self.fileBuild( params, "clean" )
248 fileClean.usage = "<bbfile>"
250 def fileEdit( self, params ):
251 """Call $EDITOR on a .bb file"""
253 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
254 fileEdit.usage = "<bbfile>"
256 def fileRebuild( self, params ):
257 """Rebuild (clean & build) a .bb file"""
258 self.fileClean( params )
259 self.fileBuild( params )
260 fileRebuild.usage = "<bbfile>"
262 def fileReparse( self, params ):
263 """(re)Parse a bb file"""
265 print "SHELL: Parsing '%s'" % bbfile
266 parse.update_mtime( bbfile )
267 cooker.bb_cache.cacheValidUpdate(bbfile)
268 fromCache = cooker.bb_cache.loadData(bbfile, cooker)
269 cooker.bb_cache.sync()
270 if False: #from Cache
271 print "SHELL: File has not been updated, not reparsing"
273 print "SHELL: Parsed"
274 fileReparse.usage = "<bbfile>"
276 def force( self, params ):
277 """Toggle force task execution flag (see bitbake -f)"""
278 cooker.configuration.force = not cooker.configuration.force
279 print "SHELL: Force Flag is now '%s'" % repr( cooker.configuration.force )
281 def help( self, params ):
282 """Show a comprehensive list of commands and their purpose"""
283 print "="*30, "Available Commands", "="*30
284 allcmds = cmds.keys()
287 function,numparams,usage,helptext = cmds[cmd]
288 print "| %s | %s" % (usage.ljust(30), helptext)
291 def lastError( self, params ):
292 """Show the reason or log that was produced by the last BitBake event exception"""
293 if last_exception is None:
294 print "SHELL: No Errors yet (Phew)..."
296 reason, event = last_exception.args
297 print "SHELL: Reason for the last error: '%s'" % reason
299 msg, filename = reason.split( ':' )
300 filename = filename.strip()
301 print "SHELL: Dumping log file for last error:"
303 print open( filename ).read()
305 print "ERROR: Couldn't open '%s'" % filename
307 def match( self, params ):
308 """Dump all files or providers matching a glob expression"""
309 what, globexpr = params
312 for key in globfilter( cooker.status.pkg_fn.keys(), globexpr ): print key
313 elif what == "providers":
315 for key in globfilter( cooker.status.pkg_pn.keys(), globexpr ): print key
317 print "Usage: match %s" % self.print_.usage
318 match.usage = "<files|providers> <glob>"
320 def new( self, params ):
321 """Create a new .bb file and open the editor"""
322 dirname, filename = params
323 packages = '/'.join( data.getVar( "BBFILES", cooker.configuration.data, 1 ).split('/')[:-2] )
324 fulldirname = "%s/%s" % ( packages, dirname )
326 if not os.path.exists( fulldirname ):
327 print "SHELL: Creating '%s'" % fulldirname
328 os.mkdir( fulldirname )
329 if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
330 if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
331 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
333 print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
334 newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
335 print >>newpackage,"""DESCRIPTION = ""
364 os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
365 new.usage = "<directory> <filename>"
367 def pasteBin( self, params ):
368 """Send a command + output buffer to http://pastebin.com"""
370 contents = self._shell.myout.buffer( int( index ) )
371 status, error, location = sendToPastebin( contents )
373 print "SHELL: Pasted to %s" % location
375 print "ERROR: %s %s" % ( status, error )
376 pasteBin.usage = "<index>"
378 def pasteLog( self, params ):
379 """Send the last event exception error log (if there is one) to http://oe.pastebin.com"""
380 if last_exception is None:
381 print "SHELL: No Errors yet (Phew)..."
383 reason, event = last_exception.args
384 print "SHELL: Reason for the last error: '%s'" % reason
386 msg, filename = reason.split( ':' )
387 filename = filename.strip()
388 print "SHELL: Pasting log file to pastebin..."
390 status, error, location = sendToPastebin( open( filename ).read() )
393 print "SHELL: Pasted to %s" % location
395 print "ERROR: %s %s" % ( status, error )
397 def patch( self, params ):
398 """Execute 'patch' command on a providee"""
399 self.build( params, "patch" )
400 patch.usage = "<providee>"
402 def parse( self, params ):
403 """(Re-)parse .bb files and calculate the dependency graph"""
404 cooker.status = cooker.ParsingStatus()
405 ignore = data.getVar("ASSUME_PROVIDED", cooker.configuration.data, 1) or ""
406 cooker.status.ignored_dependencies = set( ignore.split() )
407 cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", cooker.configuration.data, 1) )
409 cooker.collect_bbfiles( cooker.myProgressCallback )
410 cooker.buildDepgraph()
415 def reparse( self, params ):
416 """(re)Parse a providee's bb file"""
417 bbfile = self._findProvider( params[0] )
418 if bbfile is not None:
419 print "SHELL: Found bbfile '%s' for '%s'" % ( bbfile, params[0] )
420 self.fileReparse( [ bbfile ] )
422 print "ERROR: Nothing provides '%s'" % params[0]
423 reparse.usage = "<providee>"
425 def getvar( self, params ):
426 """Dump the contents of an outer BitBake environment variable"""
428 value = data.getVar( var, cooker.configuration.data, 1 )
430 getvar.usage = "<variable>"
432 def peek( self, params ):
433 """Dump contents of variable defined in providee's metadata"""
435 bbfile = self._findProvider( name )
436 if bbfile is not None:
437 the_data = cooker.bb_cache.loadDataFull(bbfile, cooker)
438 value = the_data.getVar( var, 1 )
441 print "ERROR: Nothing provides '%s'" % name
442 peek.usage = "<providee> <variable>"
444 def poke( self, params ):
445 """Set contents of variable defined in providee's metadata"""
446 name, var, value = params
447 bbfile = self._findProvider( name )
448 if bbfile is not None:
449 print "ERROR: Sorry, this functionality is currently broken"
450 #d = cooker.pkgdata[bbfile]
451 #data.setVar( var, value, d )
453 # mark the change semi persistant
454 #cooker.pkgdata.setDirty(bbfile, d)
457 print "ERROR: Nothing provides '%s'" % name
458 poke.usage = "<providee> <variable> <value>"
460 def print_( self, params ):
461 """Dump all files or providers"""
465 for key in cooker.status.pkg_fn.keys(): print key
466 elif what == "providers":
468 for key in cooker.status.providers.keys(): print key
470 print "Usage: print %s" % self.print_.usage
471 print_.usage = "<files|providers>"
473 def python( self, params ):
474 """Enter the expert mode - an interactive BitBake Python Interpreter"""
475 sys.ps1 = "EXPERT BB>>> "
476 sys.ps2 = "EXPERT BB... "
478 interpreter = code.InteractiveConsole( dict( globals() ) )
479 interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
481 def showdata( self, params ):
482 """Execute 'showdata' on a providee"""
483 self.build( params, "showdata" )
484 showdata.usage = "<providee>"
486 def setVar( self, params ):
487 """Set an outer BitBake environment variable"""
489 data.setVar( var, value, cooker.configuration.data )
491 setVar.usage = "<variable> <value>"
493 def rebuild( self, params ):
494 """Clean and rebuild a .bb file or a providee"""
495 self.build( params, "clean" )
496 self.build( params, "build" )
497 rebuild.usage = "<providee>"
499 def shell( self, params ):
500 """Execute a shell command and dump the output"""
502 print commands.getoutput( " ".join( params ) )
503 shell.usage = "<...>"
505 def stage( self, params ):
506 """Execute 'stage' on a providee"""
507 self.build( params, "stage" )
508 stage.usage = "<providee>"
510 def status( self, params ):
511 """<just for testing>"""
513 print "build cache = '%s'" % cooker.build_cache
514 print "build cache fail = '%s'" % cooker.build_cache_fail
515 print "building list = '%s'" % cooker.building_list
516 print "build path = '%s'" % cooker.build_path
517 print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
518 print "build stats = '%s'" % cooker.stats
519 if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
520 print "memory output contents = '%s'" % self._shell.myout._buffer
522 def test( self, params ):
523 """<just for testing>"""
524 print "testCommand called with '%s'" % params
526 def unpack( self, params ):
527 """Execute 'unpack' on a providee"""
528 self.build( params, "unpack" )
529 unpack.usage = "<providee>"
531 def which( self, params ):
532 """Computes the providers for a given providee"""
537 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, cooker.configuration.data, 1 )
538 if not preferred: preferred = item
541 lv, lf, pv, pf = cooker.findBestProvider( preferred )
543 lv, lf, pv, pf = (None,)*4
546 providers = cooker.status.providers[item]
548 print "SHELL: ERROR: Nothing provides", preferred
550 for provider in providers:
551 if provider == pf: provider = " (***) %s" % provider
552 else: provider = " %s" % provider
554 which.usage = "<providee>"
556 ##########################################################################
557 # Common helper functions
558 ##########################################################################
560 def completeFilePath( bbfile ):
561 """Get the complete bbfile path"""
562 if not cooker.status.pkg_fn: return bbfile
563 for key in cooker.status.pkg_fn.keys():
564 if key.endswith( bbfile ):
568 def sendToPastebin( content ):
569 """Send content to http://oe.pastebin.com"""
571 mydata["parent_pid"] = ""
572 mydata["format"] = "bash"
573 mydata["code2"] = content
574 mydata["paste"] = "Send"
575 mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
576 params = urllib.urlencode( mydata )
577 headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
579 conn = httplib.HTTPConnection( "oe.pastebin.com:80" )
580 conn.request("POST", "/", params, headers )
582 response = conn.getresponse()
585 return response.status, response.reason, response.getheader( "location" ) or "unknown"
587 def completer( text, state ):
588 """Return a possible readline completion"""
589 debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
592 line = readline.get_line_buffer()
595 # we are in second (or more) argument
596 if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
597 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
598 if u == "<variable>":
599 allmatches = cooker.configuration.data.keys()
600 elif u == "<bbfile>":
601 if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
602 else: allmatches = [ x.split("/")[-1] for x in cooker.status.pkg_fn.keys() ]
603 elif u == "<providee>":
604 if cooker.status.pkg_fn is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
605 else: allmatches = cooker.status.providers.iterkeys()
606 else: allmatches = [ "(No tab completion available for this command)" ]
607 else: allmatches = [ "(No tab completion available for this command)" ]
609 # we are in first argument
610 allmatches = cmds.iterkeys()
612 completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
613 #print "completer.matches = '%s'" % completer.matches
614 if len( completer.matches ) > state:
615 return completer.matches[state]
619 def debugOut( text ):
621 sys.stderr.write( "( %s )\n" % text )
623 def columnize( alist, width = 80 ):
625 A word-wrap function that preserves existing line breaks
626 and most spaces in the text. Expects that existing line
627 breaks are posix newlines (\n).
629 return reduce(lambda line, word, width=width: '%s%s%s' %
631 ' \n'[(len(line[line.rfind('\n')+1:])
632 + len(word.split('\n',1)[0]
638 def globfilter( names, pattern ):
639 return fnmatch.filter( names, pattern )
641 ##########################################################################
643 ##########################################################################
646 """File-like output class buffering the output of the last 10 commands"""
647 def __init__( self, delegate ):
648 self.delegate = delegate
653 def startCommand( self, command ):
654 self._command = command
656 def endCommand( self ):
657 if self._command is not None:
658 if len( self._buffer ) == 10: del self._buffer[0]
659 self._buffer.append( ( self._command, self.text ) )
660 def removeLast( self ):
662 del self._buffer[ len( self._buffer ) - 1 ]
665 def lastBuffer( self ):
667 return self._buffer[ len( self._buffer ) -1 ][1]
668 def bufferedCommands( self ):
669 return [ cmd for cmd, output in self._buffer ]
670 def buffer( self, i ):
671 if i < len( self._buffer ):
672 return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
673 else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
674 def write( self, text ):
675 if self._command is not None and text != "BB>> ": self.text.append( text )
676 if self.delegate is not None: self.delegate.write( text )
678 return self.delegate.flush()
680 return self.delegate.fileno()
682 return self.delegate.isatty()
684 ##########################################################################
686 ##########################################################################
690 def __init__( self ):
691 """Register commands and set up readline"""
692 self.commandQ = Queue.Queue()
693 self.commands = BitBakeShellCommands( self )
694 self.myout = MemoryOutput( sys.stdout )
695 self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
696 self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
698 readline.set_completer( completer )
699 readline.set_completer_delims( " " )
700 readline.parse_and_bind("tab: complete")
703 readline.read_history_file( self.historyfilename )
705 pass # It doesn't exist yet.
709 # save initial cooker configuration (will be reused in file*** commands)
711 initdata = copy.deepcopy( cooker.configuration.data )
714 """Write readline history and clean up resources"""
715 debugOut( "writing command history" )
717 readline.write_history_file( self.historyfilename )
719 print "SHELL: Unable to save command history"
721 def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
722 """Register a command"""
723 if usage == "": usage = command
724 if helptext == "": helptext = function.__doc__ or "<not yet documented>"
725 cmds[command] = ( function, numparams, usage, helptext )
727 def processCommand( self, command, params ):
728 """Process a command. Check number of params and print a usage string, if appropriate"""
729 debugOut( "processing command '%s'..." % command )
731 function, numparams, usage, helptext = cmds[command]
733 print "SHELL: ERROR: '%s' command is not a valid command." % command
734 self.myout.removeLast()
736 if (numparams != -1) and (not len( params ) == numparams):
737 print "Usage: '%s'" % usage
740 result = function( self.commands, params )
741 debugOut( "result was '%s'" % result )
743 def processStartupFile( self ):
744 """Read and execute all commands found in $HOME/.bbsh_startup"""
745 if os.path.exists( self.startupfilename ):
746 startupfile = open( self.startupfilename, "r" )
747 for cmdline in startupfile:
748 debugOut( "processing startup line '%s'" % cmdline )
752 print "ERROR: '|' in startup file is not allowed. Ignoring line"
754 self.commandQ.put( cmdline.strip() )
757 """The main command loop"""
758 while not leave_mainloop:
760 if self.commandQ.empty():
761 sys.stdout = self.myout.delegate
762 cmdline = raw_input( "BB>> " )
763 sys.stdout = self.myout
765 cmdline = self.commandQ.get()
767 allCommands = cmdline.split( ';' )
768 for command in allCommands:
771 # special case for expert mode
772 if command == 'python':
773 sys.stdout = self.myout.delegate
774 self.processCommand( command, "" )
775 sys.stdout = self.myout
777 self.myout.startCommand( command )
778 if '|' in command: # disable output
779 command, pipecmd = command.split( '|' )
780 delegate = self.myout.delegate
781 self.myout.delegate = None
782 tokens = shlex.split( command, True )
783 self.processCommand( tokens[0], tokens[1:] or "" )
784 self.myout.endCommand()
785 if pipecmd is not None: # restore output
786 self.myout.delegate = delegate
788 pipe = popen2.Popen4( pipecmd )
789 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
791 sys.stdout.write( pipe.fromchild.read() )
796 except KeyboardInterrupt:
799 ##########################################################################
800 # Start function - called from the BitBake command line utility
801 ##########################################################################
803 def start( aCooker ):
806 bbshell = BitBakeShell()
807 bbshell.processStartupFile()
811 if __name__ == "__main__":
812 print "SHELL: Sorry, this program should only be called by BitBake."