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 ##########################################################################
26 * list defined tasks per package
29 * command to reparse just one (or more) bbfile(s)
30 * automatic check if reparsing is necessary (inotify?)
31 * frontend for bb file manipulation
32 * more shell-like features:
34 - output control, i.e. pipe output into grep, sort, etc.
35 - job control, i.e. bring running commands into background and foreground
36 - wildcards for commands, i.e. build *opie*
37 * start parsing in background right after startup
38 * print variable from package data
40 * read some initial commands from startup file (batch)
43 * force doesn't always work
44 * poke doesn't work at all (outcommented atm.)
45 * readline completion for commands with more than one parameters
49 ##########################################################################
50 # Import and setup global variables
51 ##########################################################################
56 from sets import Set as set
57 import sys, os, imp, readline, socket, httplib, urllib, commands, popen2
58 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
59 from bb import data, parse, build, make, fatal
62 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
63 Type 'help' for more information, press CTRL-D to exit.""" % __version__
66 leave_mainloop = False
70 debug = os.environ.get( "BBSHELL_DEBUG", "" )
72 ##########################################################################
73 # Class BitBakeShellCommands
74 ##########################################################################
76 class BitBakeShellCommands:
77 """This class contains the valid commands for the shell"""
79 def __init__( self, shell ):
80 """Register all the commands"""
82 for attr in BitBakeShellCommands.__dict__:
83 if not attr.startswith( "_" ):
84 if attr.endswith( "_" ):
85 command = attr[:-1].lower()
87 command = attr[:].lower()
88 method = getattr( BitBakeShellCommands, attr )
89 debugOut( "registering command '%s'" % command )
90 # scan number of arguments
91 usage = getattr( method, "usage", "" )
93 numArgs = len( usage.split() )
96 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
98 def _checkParsed( self ):
100 print "SHELL: This command needs to parse bbfiles..."
103 def _findProvider( self, item ):
105 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, make.cfg, 1 )
106 if not preferred: preferred = item
108 lv, lf, pv, pf = cooker.findBestProvider( preferred )
110 lv, lf, pv, pf = (None,)*4
113 def alias( self, params ):
114 """Register a new name for a command"""
117 print "ERROR: Command '%s' not known" % old
119 cmds[new] = cmds[old]
121 alias.usage = "<alias> <command>"
123 def buffer( self, params ):
124 """Dump specified output buffer"""
126 print self._shell.myout.buffer( int( index ) )
127 buffer.usage = "<index>"
129 def buffers( self, params ):
130 """Show the available output buffers"""
131 commands = self._shell.myout.bufferedCommands()
133 print "SHELL: No buffered commands available yet. Start doing something."
135 print "="*35, "Available Output Buffers", "="*27
136 for index, cmd in enumerate( commands ):
137 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
140 def build( self, params, cmd = "build" ):
141 """Build a providee"""
144 oldcmd = make.options.cmd
145 make.options.cmd = cmd
146 cooker.build_cache = []
147 cooker.build_cache_fail = []
152 cooker.buildProvider( name )
153 except build.EventException, e:
154 print "ERROR: Couldn't build '%s'" % name
155 global last_exception
158 make.options.cmd = oldcmd
159 build.usage = "<providee>"
161 def clean( self, params ):
162 """Clean a providee"""
163 self.build( params, "clean" )
164 clean.usage = "<providee>"
166 def compile( self, params ):
167 """Execute 'compile' on a providee"""
168 self.build( params, "compile" )
169 compile.usage = "<providee>"
171 def configure( self, params ):
172 """Execute 'configure' on a providee"""
173 self.build( params, "configure" )
174 configure.usage = "<providee>"
176 def edit( self, params ):
177 """Call $EDITOR on a providee"""
179 bbfile = self._findProvider( name )
180 if bbfile is not None:
181 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), bbfile ) )
183 print "ERROR: Nothing provides '%s'" % name
184 edit.usage = "<providee>"
186 def environment( self, params ):
187 """Dump out the outer BitBake environment (see bbread)"""
188 data.emit_env(sys.__stdout__, make.cfg, True)
190 def exit_( self, params ):
191 """Leave the BitBake Shell"""
192 debugOut( "setting leave_mainloop to true" )
193 global leave_mainloop
194 leave_mainloop = True
196 def fetch( self, params ):
197 """Fetch a providee"""
198 self.build( params, "fetch" )
199 fetch.usage = "<providee>"
201 def fileBuild( self, params, cmd = "build" ):
202 """Parse and build a .bb file"""
204 bf = completeFilePath( name )
205 print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
207 oldcmd = make.options.cmd
208 make.options.cmd = cmd
209 cooker.build_cache = []
210 cooker.build_cache_fail = []
213 bbfile_data = parse.handle( bf, make.cfg )
214 except parse.ParseError:
215 print "ERROR: Unable to open or parse '%s'" % bf
217 item = data.getVar('PN', bbfile_data, 1)
218 data.setVar( "_task_cache", [], bbfile_data ) # force
220 cooker.tryBuildPackage( os.path.abspath( bf ), item, bbfile_data )
221 except build.EventException, e:
222 print "ERROR: Couldn't build '%s'" % name
223 global last_exception
226 make.options.cmd = oldcmd
227 fileBuild.usage = "<bbfile>"
229 def fileClean( self, params ):
230 """Clean a .bb file"""
231 self.fileBuild( params, "clean" )
232 fileClean.usage = "<bbfile>"
234 def fileEdit( self, params ):
235 """Call $EDITOR on a .bb file"""
237 os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
238 fileEdit.usage = "<bbfile>"
240 def fileRebuild( self, params ):
241 """Rebuild (clean & build) a .bb file"""
242 self.fileClean( params )
243 self.fileBuild( params )
244 fileRebuild.usage = "<bbfile>"
246 def force( self, params ):
247 """Toggle force task execution flag (see bitbake -f)"""
248 make.options.force = not make.options.force
249 print "SHELL: Force Flag is now '%s'" % repr( make.options.force )
251 def help( self, params ):
252 """Show a comprehensive list of commands and their purpose"""
253 print "="*30, "Available Commands", "="*30
254 allcmds = cmds.keys()
257 function,numparams,usage,helptext = cmds[cmd]
258 print "| %s | %s" % (usage.ljust(30), helptext)
261 def lastError( self, params ):
262 """Show the reason or log that was produced by the last BitBake event exception"""
263 if last_exception is None:
264 print "SHELL: No Errors yet (Phew)..."
266 reason, event = last_exception.args
267 print "SHELL: Reason for the last error: '%s'" % reason
269 msg, filename = reason.split( ':' )
270 filename = filename.strip()
271 print "SHELL: Dumping log file for last error:"
273 print open( filename ).read()
275 print "ERROR: Couldn't open '%s'" % filename
277 def new( self, params ):
278 """Create a new .bb file and open the editor"""
279 dirname, filename = params
280 packages = '/'.join( data.getVar( "BBFILES", make.cfg, 1 ).split('/')[:-2] )
281 fulldirname = "%s/%s" % ( packages, dirname )
283 if not os.path.exists( fulldirname ):
284 print "SHELL: Creating '%s'" % fulldirname
285 os.mkdir( fulldirname )
286 if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
287 if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
288 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
290 print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
291 newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
292 print >>newpackage,"""DESCRIPTION = ""
321 os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
322 new.usage = "<directory> <filename>"
324 def pasteBin( self, params ):
325 """Send a command + output buffer to http://pastebin.com"""
327 contents = self._shell.myout.buffer( int( index ) )
328 status, error, location = sendToPastebin( contents )
330 print "SHELL: Pasted to %s" % location
332 print "ERROR: %s %s" % ( status, error )
333 pasteBin.usage = "<index>"
335 def pasteLog( self, params ):
336 """Send the last event exception error log (if there is one) to http://pastebin.com"""
337 if last_exception is None:
338 print "SHELL: No Errors yet (Phew)..."
340 reason, event = last_exception.args
341 print "SHELL: Reason for the last error: '%s'" % reason
343 msg, filename = reason.split( ':' )
344 filename = filename.strip()
345 print "SHELL: Pasting log file to pastebin..."
347 status, error, location = sendToPastebin( open( filename ).read() )
350 print "SHELL: Pasted to %s" % location
352 print "ERROR: %s %s" % ( status, error )
354 def patch( self, params ):
355 """Execute 'patch' command on a providee"""
356 self.build( params, "patch" )
357 patch.usage = "<providee>"
359 def parse( self, params ):
360 """(Re-)parse .bb files and calculate the dependency graph"""
361 cooker.status = cooker.ParsingStatus()
362 ignore = data.getVar("ASSUME_PROVIDED", make.cfg, 1) or ""
363 cooker.status.ignored_dependencies = set( ignore.split() )
364 cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", make.cfg, 1) )
366 make.collect_bbfiles( cooker.myProgressCallback )
367 cooker.buildDepgraph()
372 def getvar( self, params ):
373 """Dump the contents of an outer BitBake environment variable"""
375 value = data.getVar( var, make.cfg, 1 )
377 getvar.usage = "<variable>"
379 def peek( self, params ):
380 """Dump contents of variable defined in providee's metadata"""
382 bbfile = self._findProvider( name )
383 if bbfile is not None:
384 value = make.pkgdata[bbfile].getVar( var, 1 )
387 print "ERROR: Nothing provides '%s'" % name
388 peek.usage = "<providee> <variable>"
390 #def poke( self, params ):
391 # """Set contents of variable defined in providee's metadata"""
392 # name, var, value = params
393 # bbfile = self._findProvider( name )
394 # if bbfile is not None:
395 # make.pkgdata[bbfile].setVar( var, value )
398 # print "ERROR: Nothing provides '%s'" % name
399 #poke.usage = "<providee> <variable> <value>"
401 def print_( self, params ):
402 """Dump all files or providers"""
406 for i in make.pkgdata.keys(): print i
407 elif what == "providers":
409 for i in cooker.status.providers.keys(): print i
411 print "Usage: print %s" % self.print_.usage
412 print_.usage = "<files|providers>"
414 def python( self, params ):
415 """Enter the expert mode - an interactive BitBake Python Interpreter"""
416 sys.ps1 = "EXPERT BB>>> "
417 sys.ps2 = "EXPERT BB... "
419 interpreter = code.InteractiveConsole( dict( globals() ) )
420 interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
422 def showdata( self, params ):
423 """Execute 'showdata' on a providee"""
424 self.build( params, "showdata" )
425 showdata.usage = "<providee>"
427 def setVar( self, params ):
428 """Set an outer BitBake environment variable"""
430 data.setVar( var, value, make.cfg )
432 setVar.usage = "<variable> <value>"
434 def rebuild( self, params ):
435 """Clean and rebuild a .bb file or a providee"""
436 self.build( params, "clean" )
437 self.build( params, "build" )
438 rebuild.usage = "<providee>"
440 def shell( self, params ):
441 """Execute a shell command and dump the output"""
443 print commands.getoutput( " ".join( params ) )
444 shell.usage = "<...>"
446 def stage( self, params ):
447 """Execute 'stage' on a providee"""
448 self.build( params, "stage" )
449 stage.usage = "<providee>"
451 def status( self, params ):
452 """<just for testing>"""
454 print "build cache = '%s'" % cooker.build_cache
455 print "build cache fail = '%s'" % cooker.build_cache_fail
456 print "building list = '%s'" % cooker.building_list
457 print "build path = '%s'" % cooker.build_path
458 print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
459 print "build stats = '%s'" % cooker.stats
460 if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
461 print "memory output contents = '%s'" % self._shell.myout._buffer
463 def test( self, params ):
464 """<just for testing>"""
465 print "testCommand called with '%s'" % params
467 def unpack( self, params ):
468 """Execute 'unpack' on a providee"""
469 self.build( params, "unpack" )
470 unpack.usage = "<providee>"
472 def which( self, params ):
473 """Computes the providers for a given providee"""
478 preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, make.cfg, 1 )
479 if not preferred: preferred = item
482 lv, lf, pv, pf = cooker.findBestProvider( preferred )
484 lv, lf, pv, pf = (None,)*4
487 providers = cooker.status.providers[item]
489 print "SHELL: ERROR: Nothing provides", preferred
491 for provider in providers:
492 if provider == pf: provider = " (***) %s" % provider
493 else: provider = " %s" % provider
495 which.usage = "<providee>"
497 ##########################################################################
498 # Common helper functions
499 ##########################################################################
501 def completeFilePath( bbfile ):
502 """Get the complete bbfile path"""
503 if not make.pkgdata: return bbfile
504 for key in make.pkgdata.keys():
505 if key.endswith( bbfile ):
509 def sendToPastebin( content ):
510 """Send content to http://www.pastebin.com"""
512 mydata["parent_pid"] = ""
513 mydata["format"] = "bash"
514 mydata["code2"] = content
515 mydata["paste"] = "Send"
516 mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
517 params = urllib.urlencode( mydata )
518 headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
520 conn = httplib.HTTPConnection( "pastebin.com:80" )
521 conn.request("POST", "/", params, headers )
523 response = conn.getresponse()
526 return response.status, response.reason, response.getheader( "location" ) or "unknown"
528 def completer( text, state ):
529 """Return a possible readline completion"""
530 debugOut( "completer called with text='%s', state='%d'" % ( text, state ) )
533 line = readline.get_line_buffer()
536 # we are in second (or more) argument
537 if line[0] in cmds and hasattr( cmds[line[0]][0], "usage" ): # known command and usage
538 u = getattr( cmds[line[0]][0], "usage" ).split()[0]
539 if u == "<variable>":
540 allmatches = make.cfg.keys()
541 elif u == "<bbfile>":
542 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
543 else: allmatches = [ x.split("/")[-1] for x in make.pkgdata.keys() ]
544 elif u == "<providee>":
545 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
546 else: allmatches = cooker.status.providers.iterkeys()
547 else: allmatches = [ "(No tab completion available for this command)" ]
548 else: allmatches = [ "(No tab completion available for this command)" ]
550 # we are in first argument
551 allmatches = cmds.iterkeys()
553 completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
554 #print "completer.matches = '%s'" % completer.matches
555 if len( completer.matches ) > state:
556 return completer.matches[state]
560 def debugOut( text ):
562 sys.stderr.write( "( %s )\n" % text )
564 ##########################################################################
566 ##########################################################################
569 """File-like output class buffering the output of the last 10 commands"""
570 def __init__( self, delegate ):
571 self.delegate = delegate
576 def startCommand( self, command ):
577 self._command = command
579 def endCommand( self ):
580 if self._command is not None:
581 if len( self._buffer ) == 10: del self._buffer[0]
582 self._buffer.append( ( self._command, self.text ) )
583 def removeLast( self ):
585 del self._buffer[ len( self._buffer ) - 1 ]
588 def lastBuffer( self ):
590 return self._buffer[ len( self._buffer ) -1 ][1]
591 def bufferedCommands( self ):
592 return [ cmd for cmd, output in self._buffer ]
593 def buffer( self, i ):
594 if i < len( self._buffer ):
595 return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
596 else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
597 def write( self, text ):
598 if self._command is not None and text != "BB>> ": self.text.append( text )
599 if self.delegate is not None: self.delegate.write( text )
601 return self.delegate.flush()
603 return self.delegate.fileno()
605 return self.delegate.isatty()
607 ##########################################################################
609 ##########################################################################
613 def __init__( self ):
614 """Register commands and set up readline"""
615 self.commands = BitBakeShellCommands( self )
616 self.myout = MemoryOutput( sys.stdout )
617 self.historyfilename = os.path.expanduser( "~/.bbsh_history" )
618 self.startupfilename = os.path.expanduser( "~/.bbsh_startup" )
620 readline.set_completer( completer )
621 readline.set_completer_delims( " " )
622 readline.parse_and_bind("tab: complete")
625 readline.read_history_file( self.historyfilename )
627 pass # It doesn't exist yet.
632 """Write readline history and clean up resources"""
633 debugOut( "writing command history" )
635 readline.write_history_file( self.historyfilename )
637 print "SHELL: Unable to save command history"
639 def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
640 """Register a command"""
641 if usage == "": usage = command
642 if helptext == "": helptext = function.__doc__ or "<not yet documented>"
643 cmds[command] = ( function, numparams, usage, helptext )
645 def processCommand( self, command, params ):
646 """Process a command. Check number of params and print a usage string, if appropriate"""
647 debugOut( "processing command '%s'..." % command )
649 function, numparams, usage, helptext = cmds[command]
651 print "SHELL: ERROR: '%s' command is not a valid command." % command
652 self.myout.removeLast()
654 if (numparams != -1) and (not len( params ) == numparams):
655 print "Usage: '%s'" % usage
658 result = function( self.commands, params )
659 debugOut( "result was '%s'" % result )
661 def processStartupFile( self ):
662 """Read and execute all commands found in $HOME/.bbsh_startup"""
663 if os.path.exists( self.startupfilename ):
664 startupfile = open( self.startupfilename, "r" )
665 # save_stdout = sys.stdout
666 # sys.stdout = open( "/dev/null", "w" )
668 for cmdline in startupfile.readlines():
669 cmdline = cmdline.strip()
670 debugOut( "processing startup line '%s'" % cmdline )
674 print >>save_stdout, "ERROR: ';' in startup file is not allowed. Ignoring line"
676 allCommands = cmdline.split( ';' )
677 for command in allCommands:
679 self.processCommand( command.split()[0], command.split()[1:] )
681 self.processCommand( command, "" )
683 print "SHELL: Processed %d commands from '%s'" % ( numCommands, self.startupfilename )
686 """The main command loop"""
687 while not leave_mainloop:
689 sys.stdout = self.myout.delegate
690 cmdline = raw_input( "BB>> " )
691 sys.stdout = self.myout
693 allCommands = cmdline.split( ';' )
694 for command in allCommands:
697 # special case for expert mode
698 if command == 'python':
699 sys.stdout = self.myout.delegate
700 self.processCommand( command, "" )
701 sys.stdout = self.myout
703 self.myout.startCommand( command )
704 if '|' in command: # disable output
705 command, pipecmd = command.split( '|' )
706 delegate = self.myout.delegate
707 self.myout.delegate = None
709 self.processCommand( command.split()[0], command.split()[1:] )
711 self.processCommand( command, "" )
712 self.myout.endCommand()
713 if pipecmd is not None: # restore output
714 self.myout.delegate = delegate
716 pipe = popen2.Popen4( pipecmd )
717 pipe.tochild.write( "\n".join( self.myout.lastBuffer() ) )
719 sys.stdout.write( pipe.fromchild.read() )
724 except KeyboardInterrupt:
727 ##########################################################################
728 # Start function - called from the BitBake command line utility
729 ##########################################################################
731 def start( aCooker ):
734 bbshell = BitBakeShell()
735 bbshell.processStartupFile()
739 if __name__ == "__main__":
740 print "SHELL: Sorry, this program should only be called by BitBake."