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