OO overhaul:
[vuplus_bitbake] / lib / bb / shell.py
1 #!/usr/bin/env python
2 # ex:ts=4:sw=4:sts=4:et
3 # -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*-
4 ##########################################################################
5 #
6 # Copyright (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>, Vanille Media
7 #
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.
11 #
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.
15 #
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.
19 #
20 ##########################################################################
21
22 """
23 BitBake Shell
24
25 TODO:
26     * specify tasks
27     * specify force
28     * command to reparse just one (or more) bbfile(s)
29     * automatic check if reparsing is necessary (inotify?)
30     * frontend for bb file manipulation?
31     * pipe output of commands into shell commands (i.e grep or sort)?
32     * job control, i.e. bring commands into background with '&', fg, bg, etc.?
33     * start parsing in background right after startup?
34     * print variable from package data
35     * command aliases / shortcuts?
36 """
37
38 ##########################################################################
39 # Import and setup global variables
40 ##########################################################################
41
42 try:
43     set
44 except NameError:
45     from sets import Set as set
46 import sys, os, imp, readline, socket, httplib, urllib, commands
47 imp.load_source( "bitbake", os.path.dirname( sys.argv[0] )+"/bitbake" )
48 from bb import data, parse, build, make, fatal
49
50 __version__ = "0.5.0"
51 __credits__ = """BitBake Shell Version %s (C) 2005 Michael 'Mickey' Lauer <mickey@Vanille.de>
52 Type 'help' for more information, press CTRL-D to exit.""" % __version__
53
54 cmds = {}
55 leave_mainloop = False
56 last_exception = None
57 cooker = None
58 parsed = False
59 debug = os.environ.get( "BBSHELL_DEBUG", "" ) != ""
60 history_file = "%s/.bbsh_history" % os.environ.get( "HOME" )
61
62 ##########################################################################
63 # Class BitBakeShellCommands
64 ##########################################################################
65
66 class BitBakeShellCommands:
67     """This class contains the valid commands for the shell"""
68
69     def __init__( self, shell ):
70         """Register all the commands"""
71         self._shell = shell
72         for attr in BitBakeShellCommands.__dict__:
73             if not attr.startswith( "__" ):
74                 if attr.endswith( "_" ):
75                     command = attr[:-1].lower()
76                 else:
77                     command = attr[:].lower()
78                 method = getattr( BitBakeShellCommands, attr )
79                 if debug: print "registering command '%s'" % command
80                 # scan number of arguments
81                 usage = getattr( method, "usage", "" )
82                 if usage != "<...>":
83                     numArgs = len( usage.split() )
84                 else:
85                     numArgs = -1
86                 shell.registerCommand( command, method, numArgs, "%s %s" % ( command, usage ), method.__doc__ )
87
88     def buffer( self, params ):
89         """Dump specified output buffer"""
90         index = params[0]
91         print self._shell.myout.buffer( int( index ) )
92     buffer.usage = "<index>"
93
94     def buffers( self, params ):
95         """Show the available output buffers"""
96         commands = self._shell.myout.bufferedCommands()
97         if not commands:
98             print "SHELL: No buffered commands available yet. Start doing something."
99         else:
100             print "="*35, "Available Output Buffers", "="*27
101             for index, cmd in enumerate( commands ):
102                 print "| %s %s" % ( str( index ).ljust( 3 ), cmd )
103             print "="*88
104
105     def build( self, params, cmd = "build" ):
106         """Build a providee"""
107         name = params[0]
108
109         oldcmd = make.options.cmd
110         make.options.cmd = cmd
111         cooker.build_cache = []
112         cooker.build_cache_fail = []
113
114         if not parsed:
115             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."
116             self.parse( None )
117         try:
118             cooker.buildProvider( name )
119         except build.EventException, e:
120             print "ERROR: Couldn't build '%s'" % name
121             global last_exception
122             last_exception = e
123
124         make.options.cmd = oldcmd
125     build.usage = "<providee>"
126
127     def clean( self, params ):
128         """Clean a providee"""
129         self.build( params, "clean" )
130     clean.usage = "<providee>"
131
132     def edit( self, params ):
133         """Call $EDITOR on a .bb file"""
134         name = params[0]
135         os.system( "%s %s" % ( os.environ.get( "EDITOR", "vi" ), completeFilePath( name ) ) )
136     edit.usage = "<bbfile>"
137
138     def environment( self, params ):
139         """Dump out the outer BitBake environment (see bbread)"""
140         data.emit_env(sys.__stdout__, make.cfg, True)
141
142     def exit_( self, params ):
143         """Leave the BitBake Shell"""
144         if debug: print "(setting leave_mainloop to true)"
145         global leave_mainloop
146         leave_mainloop = True
147
148     def fileBuild( self, params, cmd = "build" ):
149         """Parse and build a .bb file"""
150         name = params[0]
151         bf = completeFilePath( name )
152         print "SHELL: Calling '%s' on '%s'" % ( cmd, bf )
153
154         oldcmd = make.options.cmd
155         make.options.cmd = cmd
156         cooker.build_cache = []
157         cooker.build_cache_fail = []
158
159         try:
160             bbfile_data = parse.handle( bf, make.cfg )
161         except parse.ParseError:
162             print "ERROR: Unable to open or parse '%s'" % bf
163         else:
164             item = data.getVar('PN', bbfile_data, 1)
165             data.setVar( "_task_cache", [], bbfile_data ) # force
166             try:
167                 cooker.tryBuildPackage( os.path.abspath( bf ), item, bbfile_data )
168             except build.EventException, e:
169                 print "ERROR: Couldn't build '%s'" % name
170                 global last_exception
171                 last_exception = e
172
173         make.options.cmd = oldcmd
174     fileBuild.usage = "<bbfile>"
175
176     def fileClean( self, params ):
177         """Clean a .bb file"""
178         self.fileBuild( params, "clean" )
179     fileClean.usage = "<bbfile>"
180
181     def fileRebuild( self, params ):
182         """Rebuild (clean & build) a .bb file"""
183         self.fileClean( params )
184         self.fileBuild( params )
185     fileRebuild.usage = "<bbfile>"
186
187     def help( self, params ):
188         """Show a comprehensive list of commands and their purpose"""
189         print "="*30, "Available Commands", "="*30
190         allcmds = cmds.keys()
191         allcmds.sort()
192         for cmd in allcmds:
193             function,numparams,usage,helptext = cmds[cmd]
194             print "| %s | %s" % (usage.ljust(30), helptext)
195         print "="*78
196
197     def lastError( self, params ):
198         """Show the reason or log that was produced by the last BitBake event exception"""
199         if last_exception is None:
200             print "SHELL: No Errors yet (Phew)..."
201         else:
202             reason, event = last_exception.args
203             print "SHELL: Reason for the last error: '%s'" % reason
204             if ':' in reason:
205                 msg, filename = reason.split( ':' )
206                 filename = filename.strip()
207                 print "SHELL: Dumping log file for last error:"
208                 try:
209                     print open( filename ).read()
210                 except IOError:
211                     print "ERROR: Couldn't open '%s'" % filename
212
213     def new( self, params ):
214         """Create a new .bb file and open the editor"""
215         dirname, filename = params
216         packages = '/'.join( data.getVar( "BBFILES", make.cfg, 1 ).split('/')[:-2] )
217         fulldirname = "%s/%s" % ( packages, dirname )
218
219         if not os.path.exists( fulldirname ):
220             print "SHELL: Creating '%s'" % fulldirname
221             os.mkdir( fulldirname )
222         if os.path.exists( fulldirname ) and os.path.isdir( fulldirname ):
223             if os.path.exists( "%s/%s" % ( fulldirname, filename ) ):
224                 print "SHELL: ERROR: %s/%s already exists" % ( fulldirname, filename )
225                 return False
226             print "SHELL: Creating '%s/%s'" % ( fulldirname, filename )
227             newpackage = open( "%s/%s" % ( fulldirname, filename ), "w" )
228             print >>newpackage,"""DESCRIPTION = ""
229 SECTION = ""
230 AUTHOR = ""
231 HOMEPAGE = ""
232 MAINTAINER = ""
233 LICENSE = "GPL"
234 PR = "r0"
235
236 SRC_URI = ""
237
238 #inherit base
239
240 #do_configure() {
241 #
242 #}
243
244 #do_compile() {
245 #
246 #}
247
248 #do_stage() {
249 #
250 #}
251
252 #do_install() {
253 #
254 #}
255 """
256             newpackage.close()
257             os.system( "%s %s/%s" % ( os.environ.get( "EDITOR" ), fulldirname, filename ) )
258     new.usage = "<directory> <filename>"
259
260     def pasteBin( self, params ):
261         """Send a command + output buffer to http://pastebin.com"""
262         index = params[0]
263         contents = self._shell.myout.buffer( int( index ) )
264         status, error, location = sendToPastebin( contents )
265         if status == 302:
266             print "SHELL: Pasted to %s" % location
267         else:
268             print "ERROR: %s %s" % ( response.status, response.reason )
269     pasteBin.usage = "<index>"
270
271     def pasteLog( self, params ):
272         """Send the last event exception error log (if there is one) to http://pastebin.com"""
273         if last_exception is None:
274             print "SHELL: No Errors yet (Phew)..."
275         else:
276             reason, event = last_exception.args
277             print "SHELL: Reason for the last error: '%s'" % reason
278             if ':' in reason:
279                 msg, filename = reason.split( ':' )
280                 filename = filename.strip()
281                 print "SHELL: Pasting log file to pastebin..."
282
283                 status, error, location = sendToPastebin( open( filename ).read() )
284
285                 if status == 302:
286                     print "SHELL: Pasted to %s" % location
287                 else:
288                     print "ERROR: %s %s" % ( response.status, response.reason )
289
290     def parse( self, params ):
291         """(Re-)parse .bb files and calculate the dependency graph"""
292         cooker.status = cooker.ParsingStatus()
293         ignore = data.getVar("ASSUME_PROVIDED", make.cfg, 1) or ""
294         cooker.status.ignored_dependencies = set( ignore.split() )
295         cooker.handleCollections( data.getVar("BBFILE_COLLECTIONS", make.cfg, 1) )
296
297         make.collect_bbfiles( cooker.myProgressCallback )
298         cooker.buildDepgraph()
299         global parsed
300         parsed = True
301         print
302
303     def print_( self, params ):
304         """Print the contents of an outer BitBake environment variable"""
305         var = params[0]
306         value = data.getVar( var, make.cfg, 1 )
307         print value
308     print_.usage = "<variable>"
309
310     def python( self, params ):
311         """Enter the expert mode - an interactive BitBake Python Interpreter"""
312         sys.ps1 = "EXPERT BB>>> "
313         sys.ps2 = "EXPERT BB... "
314         import code
315         interpreter = code.InteractiveConsole( dict( globals() ) )
316         interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version )
317
318     def setVar( self, params ):
319         """Set an outer BitBake environment variable"""
320         var, value = params
321         data.setVar( var, value, make.cfg )
322         print "OK"
323     setVar.usage = "<variable> <value>"
324
325     def rebuild( self, params ):
326         """Clean and rebuild a .bb file or a providee"""
327         self.build( params, "clean" )
328         self.build( params, "build" )
329     rebuild.usage = "<providee>"
330
331     def shell( self, params ):
332         """Execute a shell command and dump the output"""
333         if params != "":
334             print commands.getoutput( " ".join( params ) )
335     shell.usage = "<...>"
336
337     def status( self, params ):
338         """<just for testing>"""
339         print "-" * 78
340         print "build cache = '%s'" % cooker.build_cache
341         print "build cache fail = '%s'" % cooker.build_cache_fail
342         print "building list = '%s'" % cooker.building_list
343         print "build path = '%s'" % cooker.build_path
344         print "consider_msgs_cache = '%s'" % cooker.consider_msgs_cache
345         print "build stats = '%s'" % cooker.stats
346         if last_exception is not None: print "last_exception = '%s'" % repr( last_exception.args )
347         print "memory output contents = '%s'" % self._shell.myout._buffer
348
349     def test( self, params ):
350         """<just for testing>"""
351         print "testCommand called with '%s'" % params
352
353     def which( self, params ):
354         """Computes the providers for a given providee"""
355         item = params[0]
356
357         if not parsed:
358             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."
359             self.parse( None )
360
361         preferred = data.getVar( "PREFERRED_PROVIDER_%s" % item, make.cfg, 1 )
362         if not preferred: preferred = item
363
364         try:
365             lv, lf, pv, pf = cooker.findBestProvider( preferred )
366         except KeyError:
367             lv, lf, pv, pf = (None,)*4
368
369         try:
370             providers = cooker.status.providers[item]
371         except KeyError:
372             print "SHELL: ERROR: Nothing provides", preferred
373         else:
374             for provider in providers:
375                 if provider == pf: provider = " (***) %s" % provider
376                 else:              provider = "       %s" % provider
377                 print provider
378     which.usage = "<providee>"
379
380 ##########################################################################
381 # Common helper functions
382 ##########################################################################
383
384 def completeFilePath( bbfile ):
385     """Get the complete bbfile path"""
386     if not make.pkgdata: return bbfile
387     for key in make.pkgdata.keys():
388         if key.endswith( bbfile ):
389             return key
390     return bbfile
391
392 def sendToPastebin( content ):
393     """Send content to http://www.pastebin.com"""
394     mydata = {}
395     mydata["parent_pid"] = ""
396     mydata["format"] = "bash"
397     mydata["code2"] = content
398     mydata["paste"] = "Send"
399     mydata["poster"] = "%s@%s" % ( os.environ.get( "USER", "unknown" ), socket.gethostname() or "unknown" )
400     params = urllib.urlencode( mydata )
401     headers = {"Content-type": "application/x-www-form-urlencoded","Accept": "text/plain"}
402
403     conn = httplib.HTTPConnection( "pastebin.com:80" )
404     conn.request("POST", "/", params, headers )
405
406     response = conn.getresponse()
407     conn.close()
408
409     return response.status, response.reason, response.getheader( "location" ) or "unknown"
410
411 def completer( text, state ):
412     """Return a possible readline completion"""
413     if debug: print "(completer called with text='%s', state='%d'" % ( text, state )
414
415     if state == 0:
416         line = readline.get_line_buffer()
417         if " " in line:
418             line = line.split()
419             # we are in second (or more) argument
420             if line[0] == "print" or line[0] == "set":
421                 allmatches = make.cfg.keys()
422             elif line[0].startswith( "file" ):
423                 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
424                 else: allmatches = [ x.split("/")[-1] for x in make.pkgdata.keys() ]
425             elif line[0] == "build" or line[0] == "clean" or line[0] == "which":
426                 if make.pkgdata is None: allmatches = [ "(No Matches Available. Parsed yet?)" ]
427                 else: allmatches = cooker.status.providers.iterkeys()
428             else: allmatches = [ "(No tab completion available for this command)" ]
429         else:
430             # we are in first argument
431             allmatches = cmds.iterkeys()
432
433         completer.matches = [ x for x in allmatches if x[:len(text)] == text ]
434         #print "completer.matches = '%s'" % completer.matches
435     if len( completer.matches ) > state:
436         return completer.matches[state]
437     else:
438         return None
439
440
441 ##########################################################################
442 # Class MemoryOutput
443 ##########################################################################
444
445 class MemoryOutput:
446     """File-like output class buffering the output of the last 10 commands"""
447     def __init__( self, delegate ):
448         self.delegate = delegate
449         self._buffer = []
450         self.text = []
451         self._command = None
452
453     def startCommand( self, command ):
454         self._command = command
455         self.text = []
456     def endCommand( self ):
457         if self._command is not None:
458             if len( self._buffer ) == 10: del self._buffer[0]
459             self._buffer.append( ( self._command, self.text ) )
460     def removeLast( self ):
461         if self._buffer:
462             del self._buffer[ len( self._buffer ) - 1 ]
463         self.text = []
464         self._command = None
465     def bufferedCommands( self ):
466         return [ cmd for cmd, output in self._buffer ]
467     def buffer( self, i ):
468         if i < len( self._buffer ):
469             return "BB>> %s\n%s" % ( self._buffer[i][0], "".join( self._buffer[i][1] ) )
470         else: return "ERROR: Invalid buffer number. Buffer needs to be in (0, %d)" % ( len( self._buffer ) - 1 )
471     def write( self, text ):
472         if self._command is not None and text != "BB>> ": self.text.append( text )
473         self.delegate.write( text )
474     def flush( self ):
475         return self.delegate.flush()
476     def fileno( self ):
477         return self.delegate.fileno()
478     def isatty( self ):
479         return self.delegate.isatty()
480
481 ##########################################################################
482 # Class BitBakeShell
483 ##########################################################################
484
485 class BitBakeShell:
486
487     def __init__( self ):
488         """Register commands and set up readline"""
489         self.commands = BitBakeShellCommands( self )
490         self.myout = MemoryOutput( sys.stdout )
491
492         readline.set_completer( completer )
493         readline.set_completer_delims( " " )
494         readline.parse_and_bind("tab: complete")
495
496         try:
497             global history_file
498             readline.read_history_file( history_file )
499         except IOError:
500             pass  # It doesn't exist yet.
501
502         print __credits__
503
504     def cleanup( self ):
505         """Write readline history and clean up resources"""
506         if debug: print "(writing command history)"
507         try:
508             global history_file
509             readline.write_history_file( history_file )
510         except:
511             print "SHELL: Unable to save command history"
512
513     def registerCommand( self, command, function, numparams = 0, usage = "", helptext = "" ):
514         """Register a command"""
515         if usage == "": usage = command
516         if helptext == "": helptext = function.__doc__ or "<not yet documented>"
517         cmds[command] = ( function, numparams, usage, helptext )
518
519     def processCommand( self, command, params ):
520         """Process a command. Check number of params and print a usage string, if appropriate"""
521         if debug: print "(processing command '%s'...)" % command
522         try:
523             function, numparams, usage, helptext = cmds[command]
524         except KeyError:
525             print "SHELL: ERROR: '%s' command is not a valid command." % command
526             self.myout.removeLast()
527         else:
528             if (numparams != -1) and (not len( params ) == numparams):
529                 print "Usage: '%s'" % usage
530                 return
531
532             result = function( self.commands, params )
533             if debug: print "(result was '%s')" % result
534
535     def main( self ):
536         """The main command loop"""
537         while not leave_mainloop:
538             try:
539                 sys.stdout = self.myout.delegate
540                 cmdline = raw_input( "BB>> " )
541                 sys.stdout = self.myout
542                 if cmdline:
543                     commands = cmdline.split( ';' )
544                     for command in commands:
545                         self.myout.startCommand( command )
546                         if ' ' in command:
547                             self.processCommand( command.split()[0], command.split()[1:] )
548                         else:
549                             self.processCommand( command, "" )
550                         self.myout.endCommand()
551             except EOFError:
552                 print
553                 return
554             except KeyboardInterrupt:
555                 print
556
557 ##########################################################################
558 # Start function - called from the BitBake command line utility
559 ##########################################################################
560
561 def start( aCooker ):
562     global cooker
563     cooker = aCooker
564     bbshell = BitBakeShell()
565     bbshell.main()
566     bbshell.cleanup()
567
568 if __name__ == "__main__":
569     print "SHELL: Sorry, this program should only be called by BitBake."