From 22c29d8651668195f72e2f6a8e059d625eb511c3 Mon Sep 17 00:00:00 2001 From: Richard Purdie Date: Wed, 20 Jan 2010 18:46:02 +0000 Subject: bitbake: Switch to bitbake-dev version (bitbake master upstream) Signed-off-by: Richard Purdie --- bitbake/lib/bb/__init__.py | 3 +- bitbake/lib/bb/build.py | 226 ++++---- bitbake/lib/bb/cache.py | 89 ++-- bitbake/lib/bb/command.py | 271 ++++++++++ bitbake/lib/bb/cooker.py | 761 +++++++++++++++++---------- bitbake/lib/bb/daemonize.py | 191 +++++++ bitbake/lib/bb/data.py | 2 +- bitbake/lib/bb/event.py | 211 ++++---- bitbake/lib/bb/fetch/__init__.py | 42 +- bitbake/lib/bb/fetch/cvs.py | 2 +- bitbake/lib/bb/fetch/git.py | 73 ++- bitbake/lib/bb/fetch/local.py | 4 +- bitbake/lib/bb/fetch/svk.py | 2 +- bitbake/lib/bb/fetch/wget.py | 2 +- bitbake/lib/bb/msg.py | 26 +- bitbake/lib/bb/parse/parse_py/BBHandler.py | 32 +- bitbake/lib/bb/parse/parse_py/ConfHandler.py | 13 +- bitbake/lib/bb/providers.py | 4 +- bitbake/lib/bb/runqueue.py | 341 ++++++++---- bitbake/lib/bb/server/__init__.py | 2 + bitbake/lib/bb/server/none.py | 181 +++++++ bitbake/lib/bb/server/xmlrpc.py | 187 +++++++ bitbake/lib/bb/shell.py | 19 +- bitbake/lib/bb/taskdata.py | 38 +- bitbake/lib/bb/ui/__init__.py | 18 + bitbake/lib/bb/ui/crumbs/__init__.py | 18 + bitbake/lib/bb/ui/crumbs/buildmanager.py | 457 ++++++++++++++++ bitbake/lib/bb/ui/crumbs/puccho.glade | 606 +++++++++++++++++++++ bitbake/lib/bb/ui/crumbs/runningbuild.py | 180 +++++++ bitbake/lib/bb/ui/depexp.py | 272 ++++++++++ bitbake/lib/bb/ui/goggle.py | 77 +++ bitbake/lib/bb/ui/knotty.py | 162 ++++++ bitbake/lib/bb/ui/ncurses.py | 335 ++++++++++++ bitbake/lib/bb/ui/puccho.py | 425 +++++++++++++++ bitbake/lib/bb/ui/uievent.py | 125 +++++ bitbake/lib/bb/ui/uihelper.py | 49 ++ bitbake/lib/bb/utils.py | 20 +- 37 files changed, 4731 insertions(+), 735 deletions(-) create mode 100644 bitbake/lib/bb/command.py create mode 100644 bitbake/lib/bb/daemonize.py create mode 100644 bitbake/lib/bb/server/__init__.py create mode 100644 bitbake/lib/bb/server/none.py create mode 100644 bitbake/lib/bb/server/xmlrpc.py create mode 100644 bitbake/lib/bb/ui/__init__.py create mode 100644 bitbake/lib/bb/ui/crumbs/__init__.py create mode 100644 bitbake/lib/bb/ui/crumbs/buildmanager.py create mode 100644 bitbake/lib/bb/ui/crumbs/puccho.glade create mode 100644 bitbake/lib/bb/ui/crumbs/runningbuild.py create mode 100644 bitbake/lib/bb/ui/depexp.py create mode 100644 bitbake/lib/bb/ui/goggle.py create mode 100644 bitbake/lib/bb/ui/knotty.py create mode 100644 bitbake/lib/bb/ui/ncurses.py create mode 100644 bitbake/lib/bb/ui/puccho.py create mode 100644 bitbake/lib/bb/ui/uievent.py create mode 100644 bitbake/lib/bb/ui/uihelper.py (limited to 'bitbake/lib') diff --git a/bitbake/lib/bb/__init__.py b/bitbake/lib/bb/__init__.py index b8f7c7f59e..f2f8f656d8 100644 --- a/bitbake/lib/bb/__init__.py +++ b/bitbake/lib/bb/__init__.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -__version__ = "1.8.13" +__version__ = "1.9.0" __all__ = [ @@ -54,6 +54,7 @@ __all__ = [ # modules "parse", "data", + "command", "event", "build", "fetch", diff --git a/bitbake/lib/bb/build.py b/bitbake/lib/bb/build.py index 1d6742b6e6..6d80b4b549 100644 --- a/bitbake/lib/bb/build.py +++ b/bitbake/lib/bb/build.py @@ -25,8 +25,8 @@ # #Based on functions from the base bb module, Copyright 2003 Holger Schurig -from bb import data, fetch, event, mkdirhier, utils -import bb, os +from bb import data, event, mkdirhier, utils +import bb, os, sys # When we execute a python function we'd like certain things # in all namespaces, hence we add them to __builtins__ @@ -37,7 +37,11 @@ __builtins__['os'] = os # events class FuncFailed(Exception): - """Executed function failed""" + """ + Executed function failed + First parameter a message + Second paramter is a logfile (optional) + """ class EventException(Exception): """Exception which is associated with an Event.""" @@ -50,7 +54,9 @@ class TaskBase(event.Event): def __init__(self, t, d ): self._task = t - event.Event.__init__(self, d) + self._package = bb.data.getVar("PF", d, 1) + event.Event.__init__(self) + self._message = "package %s: task %s: %s" % (bb.data.getVar("PF", d, 1), t, bb.event.getName(self)[4:]) def getTask(self): return self._task @@ -68,6 +74,10 @@ class TaskSucceeded(TaskBase): class TaskFailed(TaskBase): """Task execution failed""" + def __init__(self, msg, logfile, t, d ): + self.logfile = logfile + self.msg = msg + TaskBase.__init__(self, t, d) class InvalidTask(TaskBase): """Invalid Task""" @@ -104,42 +114,116 @@ def exec_func(func, d, dirs = None): else: adir = data.getVar('B', d, 1) + # Save current directory try: prevdir = os.getcwd() except OSError: prevdir = data.getVar('TOPDIR', d, True) + + # Setup logfiles + t = data.getVar('T', d, 1) + if not t: + bb.msg.fatal(bb.msg.domain.Build, "T not set") + mkdirhier(t) + # Gross hack, FIXME + import random + logfile = "%s/log.%s.%s.%s" % (t, func, str(os.getpid()),random.random()) + runfile = "%s/run.%s.%s" % (t, func, str(os.getpid())) + + # Change to correct directory (if specified) if adir and os.access(adir, os.F_OK): os.chdir(adir) + # Handle logfiles + si = file('/dev/null', 'r') + try: + if bb.msg.debug_level['default'] > 0 or ispython: + so = os.popen("tee \"%s\"" % logfile, "w") + else: + so = file(logfile, 'w') + except OSError, e: + bb.msg.error(bb.msg.domain.Build, "opening log file: %s" % e) + pass + + se = so + + # Dup the existing fds so we dont lose them + osi = [os.dup(sys.stdin.fileno()), sys.stdin.fileno()] + oso = [os.dup(sys.stdout.fileno()), sys.stdout.fileno()] + ose = [os.dup(sys.stderr.fileno()), sys.stderr.fileno()] + + # Replace those fds with our own + os.dup2(si.fileno(), osi[1]) + os.dup2(so.fileno(), oso[1]) + os.dup2(se.fileno(), ose[1]) + locks = [] lockfiles = (data.expand(flags['lockfiles'], d) or "").split() for lock in lockfiles: locks.append(bb.utils.lockfile(lock)) - if flags['python']: - exec_func_python(func, d) - else: - exec_func_shell(func, d, flags) + try: + # Run the function + if ispython: + exec_func_python(func, d, runfile, logfile) + else: + exec_func_shell(func, d, runfile, logfile, flags) + + # Restore original directory + try: + os.chdir(prevdir) + except: + pass - for lock in locks: - bb.utils.unlockfile(lock) + finally: - if os.path.exists(prevdir): - os.chdir(prevdir) + # Unlock any lockfiles + for lock in locks: + bb.utils.unlockfile(lock) + + # Restore the backup fds + os.dup2(osi[0], osi[1]) + os.dup2(oso[0], oso[1]) + os.dup2(ose[0], ose[1]) + + # Close our logs + si.close() + so.close() + se.close() -def exec_func_python(func, d): + if os.path.exists(logfile) and os.path.getsize(logfile) == 0: + bb.msg.debug(2, bb.msg.domain.Build, "Zero size logfile %s, removing" % logfile) + os.remove(logfile) + + # Close the backup fds + os.close(osi[0]) + os.close(oso[0]) + os.close(ose[0]) + +def exec_func_python(func, d, runfile, logfile): """Execute a python BB 'function'""" - import re + import re, os bbfile = bb.data.getVar('FILE', d, 1) tmp = "def " + func + "():\n%s" % data.getVar(func, d) tmp += '\n' + func + '()' + + f = open(runfile, "w") + f.write(tmp) comp = utils.better_compile(tmp, func, bbfile) g = {} # globals g['d'] = d - utils.better_exec(comp, g, tmp, bbfile) + try: + utils.better_exec(comp, g, tmp, bbfile) + except: + (t,value,tb) = sys.exc_info() + + if t in [bb.parse.SkipPackage, bb.build.FuncFailed]: + raise + bb.msg.error(bb.msg.domain.Build, "Function %s failed" % func) + raise FuncFailed("function %s failed" % func, logfile) -def exec_func_shell(func, d, flags): +def exec_func_shell(func, d, runfile, logfile, flags): """Execute a shell BB 'function' Returns true if execution was successful. For this, it creates a bash shell script in the tmp dectory, writes the local @@ -149,23 +233,13 @@ def exec_func_shell(func, d, flags): of the directories you need created prior to execution. The last item in the list is where we will chdir/cd to. """ - import sys deps = flags['deps'] check = flags['check'] - interact = flags['interactive'] if check in globals(): if globals()[check](func, deps): return - global logfile - t = data.getVar('T', d, 1) - if not t: - return 0 - mkdirhier(t) - logfile = "%s/log.%s.%s" % (t, func, str(os.getpid())) - runfile = "%s/run.%s.%s" % (t, func, str(os.getpid())) - f = open(runfile, "w") f.write("#!/bin/sh -e\n") if bb.msg.debug_level['default'] > 0: f.write("set -x\n") @@ -177,91 +251,21 @@ def exec_func_shell(func, d, flags): os.chmod(runfile, 0775) if not func: bb.msg.error(bb.msg.domain.Build, "Function not specified") - raise FuncFailed() - - # open logs - si = file('/dev/null', 'r') - try: - if bb.msg.debug_level['default'] > 0: - so = os.popen("tee \"%s\"" % logfile, "w") - else: - so = file(logfile, 'w') - except OSError, e: - bb.msg.error(bb.msg.domain.Build, "opening log file: %s" % e) - pass - - se = so - - if not interact: - # dup the existing fds so we dont lose them - osi = [os.dup(sys.stdin.fileno()), sys.stdin.fileno()] - oso = [os.dup(sys.stdout.fileno()), sys.stdout.fileno()] - ose = [os.dup(sys.stderr.fileno()), sys.stderr.fileno()] - - # replace those fds with our own - os.dup2(si.fileno(), osi[1]) - os.dup2(so.fileno(), oso[1]) - os.dup2(se.fileno(), ose[1]) + raise FuncFailed("Function not specified for exec_func_shell") # execute function - prevdir = os.getcwd() if flags['fakeroot']: maybe_fakeroot = "PATH=\"%s\" fakeroot " % bb.data.getVar("PATH", d, 1) else: maybe_fakeroot = '' lang_environment = "LC_ALL=C " ret = os.system('%s%ssh -e %s' % (lang_environment, maybe_fakeroot, runfile)) - try: - os.chdir(prevdir) - except: - pass - - if not interact: - # restore the backups - os.dup2(osi[0], osi[1]) - os.dup2(oso[0], oso[1]) - os.dup2(ose[0], ose[1]) - # close our logs - si.close() - so.close() - se.close() - - if os.path.exists(logfile) and os.path.getsize(logfile) == 0: - bb.msg.debug(2, bb.msg.domain.Build, "Zero size logfile %s, removing" % logfile) - os.remove(logfile) - - # close the backup fds - os.close(osi[0]) - os.close(oso[0]) - os.close(ose[0]) - - if ret==0: - if bb.msg.debug_level['default'] > 0: - os.remove(runfile) -# os.remove(logfile) + if ret == 0: return - else: - bb.msg.error(bb.msg.domain.Build, "function %s failed" % func) - if data.getVar("BBINCLUDELOGS", d): - bb.msg.error(bb.msg.domain.Build, "log data follows (%s)" % logfile) - number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d) - if number_of_lines: - os.system('tail -n%s %s' % (number_of_lines, logfile)) - elif os.path.exists(logfile): - f = open(logfile, "r") - while True: - l = f.readline() - if l == '': - break - l = l.rstrip() - print '| %s' % l - f.close() - else: - bb.msg.error(bb.msg.domain.Build, "There was no logfile output") - else: - bb.msg.error(bb.msg.domain.Build, "see log in %s" % logfile) - raise FuncFailed( logfile ) + + bb.msg.error(bb.msg.domain.Build, "Function %s failed" % func) + raise FuncFailed("function %s failed" % func, logfile) def exec_task(task, d): @@ -282,14 +286,20 @@ def exec_task(task, d): data.setVar('OVERRIDES', 'task-%s:%s' % (task[3:], old_overrides), localdata) data.update_data(localdata) data.expandKeys(localdata) - event.fire(TaskStarted(task, localdata)) + event.fire(TaskStarted(task, localdata), localdata) exec_func(task, localdata) - event.fire(TaskSucceeded(task, localdata)) - except FuncFailed, reason: - bb.msg.note(1, bb.msg.domain.Build, "Task failed: %s" % reason ) - failedevent = TaskFailed(task, d) - event.fire(failedevent) - raise EventException("Function failed in task: %s" % reason, failedevent) + event.fire(TaskSucceeded(task, localdata), localdata) + except FuncFailed, message: + # Try to extract the optional logfile + try: + (msg, logfile) = message + except: + logfile = None + msg = message + bb.msg.note(1, bb.msg.domain.Build, "Task failed: %s" % message ) + failedevent = TaskFailed(msg, logfile, task, d) + event.fire(failedevent, d) + raise EventException("Function failed in task: %s" % message, failedevent) # make stamp, or cause event and raise exception if not data.getVarFlag(task, 'nostamp', d) and not data.getVarFlag(task, 'selfstamp', d): diff --git a/bitbake/lib/bb/cache.py b/bitbake/lib/bb/cache.py index d30d57d33b..2f1b8fa601 100644 --- a/bitbake/lib/bb/cache.py +++ b/bitbake/lib/bb/cache.py @@ -134,7 +134,18 @@ class Cache: self.data = data # Make sure __depends makes the depends_cache - self.getVar("__depends", virtualfn, True) + # If we're a virtual class we need to make sure all our depends are appended + # to the depends of fn. + depends = self.getVar("__depends", virtualfn, True) or [] + if "__depends" not in self.depends_cache[fn] or not self.depends_cache[fn]["__depends"]: + self.depends_cache[fn]["__depends"] = depends + for dep in depends: + if dep not in self.depends_cache[fn]["__depends"]: + self.depends_cache[fn]["__depends"].append(dep) + + # Make sure BBCLASSEXTEND always makes the cache too + self.getVar('BBCLASSEXTEND', virtualfn, True) + self.depends_cache[virtualfn]["CACHETIMESTAMP"] = bb.parse.cached_mtime(fn) def virtualfn2realfn(self, virtualfn): @@ -170,11 +181,8 @@ class Cache: bb.msg.debug(1, bb.msg.domain.Cache, "Parsing %s (full)" % fn) - bb_data, skipped = self.load_bbfile(fn, cfgData) - if isinstance(bb_data, dict): - return bb_data[cls] - - return bb_data + bb_data = self.load_bbfile(fn, cfgData) + return bb_data[cls] def loadData(self, fn, cfgData, cacheData): """ @@ -184,42 +192,39 @@ class Cache: to record the variables accessed. Return the cache status and whether the file was skipped when parsed """ + skipped = 0 + virtuals = 0 + if fn not in self.checked: self.cacheValidUpdate(fn) + if self.cacheValid(fn): - if "SKIPPED" in self.depends_cache[fn]: - return True, True - self.handle_data(fn, cacheData) multi = self.getVar('BBCLASSEXTEND', fn, True) - if multi: - for cls in multi.split(): - virtualfn = self.realfn2virtual(fn, cls) - # Pretend we're clean so getVar works - self.clean[virtualfn] = "" - self.handle_data(virtualfn, cacheData) - return True, False + for cls in (multi or "").split() + [""]: + virtualfn = self.realfn2virtual(fn, cls) + if self.depends_cache[virtualfn]["__SKIPPED"]: + skipped += 1 + bb.msg.debug(1, bb.msg.domain.Cache, "Skipping %s" % virtualfn) + continue + self.handle_data(virtualfn, cacheData) + virtuals += 1 + return True, skipped, virtuals bb.msg.debug(1, bb.msg.domain.Cache, "Parsing %s" % fn) - bb_data, skipped = self.load_bbfile(fn, cfgData) - - if skipped: - if isinstance(bb_data, dict): - self.setData(fn, fn, bb_data[""]) - else: - self.setData(fn, fn, bb_data) - return False, skipped + bb_data = self.load_bbfile(fn, cfgData) - if isinstance(bb_data, dict): - for data in bb_data: - virtualfn = self.realfn2virtual(fn, data) - self.setData(virtualfn, fn, bb_data[data]) + for data in bb_data: + virtualfn = self.realfn2virtual(fn, data) + self.setData(virtualfn, fn, bb_data[data]) + if self.getVar("__SKIPPED", virtualfn, True): + skipped += 1 + bb.msg.debug(1, bb.msg.domain.Cache, "Skipping %s" % virtualfn) + else: self.handle_data(virtualfn, cacheData) - return False, skipped + virtuals += 1 + return False, skipped, virtuals - self.setData(fn, fn, bb_data) - self.handle_data(fn, cacheData) - return False, skipped def cacheValid(self, fn): """ @@ -286,16 +291,13 @@ class Cache: if not fn in self.clean: self.clean[fn] = "" - return True + # Mark extended class data as clean too + multi = self.getVar('BBCLASSEXTEND', fn, True) + for cls in (multi or "").split(): + virtualfn = self.realfn2virtual(fn, cls) + self.clean[virtualfn] = "" - def skip(self, fn): - """ - Mark a fn as skipped - Called from the parser - """ - if not fn in self.depends_cache: - self.depends_cache[fn] = {} - self.depends_cache[fn]["SKIPPED"] = "1" + return True def remove(self, fn): """ @@ -462,10 +464,7 @@ class Cache: try: bb_data = parse.handle(bbfile, bb_data) # read .bb data os.chdir(oldpath) - return bb_data, False - except bb.parse.SkipPackage: - os.chdir(oldpath) - return bb_data, True + return bb_data except: os.chdir(oldpath) raise diff --git a/bitbake/lib/bb/command.py b/bitbake/lib/bb/command.py new file mode 100644 index 0000000000..2bb5365c0c --- /dev/null +++ b/bitbake/lib/bb/command.py @@ -0,0 +1,271 @@ +""" +BitBake 'Command' module + +Provide an interface to interact with the bitbake server through 'commands' +""" + +# Copyright (C) 2006-2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" +The bitbake server takes 'commands' from its UI/commandline. +Commands are either synchronous or asynchronous. +Async commands return data to the client in the form of events. +Sync commands must only return data through the function return value +and must not trigger events, directly or indirectly. +Commands are queued in a CommandQueue +""" + +import bb + +async_cmds = {} +sync_cmds = {} + +class Command: + """ + A queue of asynchronous commands for bitbake + """ + def __init__(self, cooker): + + self.cooker = cooker + self.cmds_sync = CommandsSync() + self.cmds_async = CommandsAsync() + + # FIXME Add lock for this + self.currentAsyncCommand = None + + for attr in CommandsSync.__dict__: + command = attr[:].lower() + method = getattr(CommandsSync, attr) + sync_cmds[command] = (method) + + for attr in CommandsAsync.__dict__: + command = attr[:].lower() + method = getattr(CommandsAsync, attr) + async_cmds[command] = (method) + + def runCommand(self, commandline): + try: + command = commandline.pop(0) + if command in CommandsSync.__dict__: + # Can run synchronous commands straight away + return getattr(CommandsSync, command)(self.cmds_sync, self, commandline) + if self.currentAsyncCommand is not None: + return "Busy (%s in progress)" % self.currentAsyncCommand[0] + if command not in CommandsAsync.__dict__: + return "No such command" + self.currentAsyncCommand = (command, commandline) + self.cooker.server.register_idle_function(self.cooker.runCommands, self.cooker) + return True + except: + import traceback + return traceback.format_exc() + + def runAsyncCommand(self): + try: + if self.currentAsyncCommand is not None: + (command, options) = self.currentAsyncCommand + commandmethod = getattr(CommandsAsync, command) + needcache = getattr( commandmethod, "needcache" ) + if needcache and self.cooker.cookerState != bb.cooker.cookerParsed: + self.cooker.updateCache() + return True + else: + commandmethod(self.cmds_async, self, options) + return False + else: + return False + except: + import traceback + self.finishAsyncCommand(traceback.format_exc()) + return False + + def finishAsyncCommand(self, error = None): + if error: + bb.event.fire(bb.command.CookerCommandFailed(error), self.cooker.configuration.event_data) + else: + bb.event.fire(bb.command.CookerCommandCompleted(), self.cooker.configuration.event_data) + self.currentAsyncCommand = None + + +class CommandsSync: + """ + A class of synchronous commands + These should run quickly so as not to hurt interactive performance. + These must not influence any running synchronous command. + """ + + def stateShutdown(self, command, params): + """ + Trigger cooker 'shutdown' mode + """ + command.cooker.cookerAction = bb.cooker.cookerShutdown + + def stateStop(self, command, params): + """ + Stop the cooker + """ + command.cooker.cookerAction = bb.cooker.cookerStop + + def getCmdLineAction(self, command, params): + """ + Get any command parsed from the commandline + """ + return command.cooker.commandlineAction + + def getVariable(self, command, params): + """ + Read the value of a variable from configuration.data + """ + varname = params[0] + expand = True + if len(params) > 1: + expand = params[1] + + return bb.data.getVar(varname, command.cooker.configuration.data, expand) + + def setVariable(self, command, params): + """ + Set the value of variable in configuration.data + """ + varname = params[0] + value = params[1] + bb.data.setVar(varname, value, command.cooker.configuration.data) + + +class CommandsAsync: + """ + A class of asynchronous commands + These functions communicate via generated events. + Any function that requires metadata parsing should be here. + """ + + def buildFile(self, command, params): + """ + Build a single specified .bb file + """ + bfile = params[0] + task = params[1] + + command.cooker.buildFile(bfile, task) + buildFile.needcache = False + + def buildTargets(self, command, params): + """ + Build a set of targets + """ + pkgs_to_build = params[0] + task = params[1] + + command.cooker.buildTargets(pkgs_to_build, task) + buildTargets.needcache = True + + def generateDepTreeEvent(self, command, params): + """ + Generate an event containing the dependency information + """ + pkgs_to_build = params[0] + task = params[1] + + command.cooker.generateDepTreeEvent(pkgs_to_build, task) + command.finishAsyncCommand() + generateDepTreeEvent.needcache = True + + def generateDotGraph(self, command, params): + """ + Dump dependency information to disk as .dot files + """ + pkgs_to_build = params[0] + task = params[1] + + command.cooker.generateDotGraphFiles(pkgs_to_build, task) + command.finishAsyncCommand() + generateDotGraph.needcache = True + + def showVersions(self, command, params): + """ + Show the currently selected versions + """ + command.cooker.showVersions() + command.finishAsyncCommand() + showVersions.needcache = True + + def showEnvironmentTarget(self, command, params): + """ + Print the environment of a target recipe + (needs the cache to work out which recipe to use) + """ + pkg = params[0] + + command.cooker.showEnvironment(None, pkg) + command.finishAsyncCommand() + showEnvironmentTarget.needcache = True + + def showEnvironment(self, command, params): + """ + Print the standard environment + or if specified the environment for a specified recipe + """ + bfile = params[0] + + command.cooker.showEnvironment(bfile) + command.finishAsyncCommand() + showEnvironment.needcache = False + + def parseFiles(self, command, params): + """ + Parse the .bb files + """ + command.cooker.updateCache() + command.finishAsyncCommand() + parseFiles.needcache = True + + def compareRevisions(self, command, params): + """ + Parse the .bb files + """ + command.cooker.compareRevisions() + command.finishAsyncCommand() + compareRevisions.needcache = True + +# +# Events +# +class CookerCommandCompleted(bb.event.Event): + """ + Cooker command completed + """ + def __init__(self): + bb.event.Event.__init__(self) + + +class CookerCommandFailed(bb.event.Event): + """ + Cooker command completed + """ + def __init__(self, error): + bb.event.Event.__init__(self) + self.error = error + +class CookerCommandSetExitCode(bb.event.Event): + """ + Set the exit code for a cooker command + """ + def __init__(self, exitcode): + bb.event.Event.__init__(self) + self.exitcode = int(exitcode) + + + diff --git a/bitbake/lib/bb/cooker.py b/bitbake/lib/bb/cooker.py index 14ccfb59aa..8036d7e9d5 100644 --- a/bitbake/lib/bb/cooker.py +++ b/bitbake/lib/bb/cooker.py @@ -7,7 +7,7 @@ # Copyright (C) 2003 - 2005 Michael 'Mickey' Lauer # Copyright (C) 2005 Holger Hans Peter Freyther # Copyright (C) 2005 ROAD GmbH -# Copyright (C) 2006 Richard Purdie +# Copyright (C) 2006 - 2007 Richard Purdie # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 as @@ -25,9 +25,35 @@ import sys, os, getopt, glob, copy, os.path, re, time import bb from bb import utils, data, parse, event, cache, providers, taskdata, runqueue +from bb import command +import bb.server.xmlrpc import itertools, sre_constants -parsespin = itertools.cycle( r'|/-\\' ) +class MultipleMatches(Exception): + """ + Exception raised when multiple file matches are found + """ + +class ParsingErrorsFound(Exception): + """ + Exception raised when parsing errors are found + """ + +class NothingToBuild(Exception): + """ + Exception raised when there is nothing to build + """ + + +# Different states cooker can be in +cookerClean = 1 +cookerParsing = 2 +cookerParsed = 3 + +# Different action states the cooker can be in +cookerRun = 1 # Cooker is running normally +cookerShutdown = 2 # Active tasks should be brought to a controlled stop +cookerStop = 3 # Stop, now! #============================================================================# # BBCooker @@ -37,12 +63,14 @@ class BBCooker: Manages one bitbake build run """ - def __init__(self, configuration): + def __init__(self, configuration, server): self.status = None self.cache = None self.bb_cache = None + self.server = server.BitBakeServer(self) + self.configuration = configuration if self.configuration.verbose: @@ -58,17 +86,15 @@ class BBCooker: self.configuration.data = bb.data.init() - def parseConfiguration(self): - bb.data.inheritFromOS(self.configuration.data) - # Add conf/bitbake.conf to the list of configuration files to read - self.configuration.file.append( os.path.join( "conf", "bitbake.conf" ) ) + for f in self.configuration.file: + self.parseConfigurationFile( f ) - self.parseConfigurationFile(self.configuration.file) + self.parseConfigurationFile( os.path.join( "conf", "bitbake.conf" ) ) if not self.configuration.cmd: - self.configuration.cmd = bb.data.getVar("BB_DEFAULT_TASK", self.configuration.data) or "build" + self.configuration.cmd = bb.data.getVar("BB_DEFAULT_TASK", self.configuration.data, True) or "build" bbpkgs = bb.data.getVar('BBPKGS', self.configuration.data, True) if bbpkgs and len(self.configuration.pkgs_to_build) == 0: @@ -80,9 +106,7 @@ class BBCooker: self.configuration.event_data = bb.data.createCopy(self.configuration.data) bb.data.update_data(self.configuration.event_data) - # # TOSTOP must not be set or our children will hang when they output - # fd = sys.stdout.fileno() if os.isatty(fd): import termios @@ -92,40 +116,91 @@ class BBCooker: tcattr[3] = tcattr[3] & ~termios.TOSTOP termios.tcsetattr(fd, termios.TCSANOW, tcattr) + self.command = bb.command.Command(self) + self.cookerState = cookerClean + self.cookerAction = cookerRun + + def parseConfiguration(self): + + # Change nice level if we're asked to nice = bb.data.getVar("BB_NICE_LEVEL", self.configuration.data, True) if nice: curnice = os.nice(0) nice = int(nice) - curnice bb.msg.note(2, bb.msg.domain.Build, "Renice to %s " % os.nice(nice)) - + + def parseCommandLine(self): + # Parse any commandline into actions + if self.configuration.show_environment: + self.commandlineAction = None + + if 'world' in self.configuration.pkgs_to_build: + bb.error("'world' is not a valid target for --environment.") + elif len(self.configuration.pkgs_to_build) > 1: + bb.error("Only one target can be used with the --environment option.") + elif self.configuration.buildfile and len(self.configuration.pkgs_to_build) > 0: + bb.error("No target should be used with the --environment and --buildfile options.") + elif len(self.configuration.pkgs_to_build) > 0: + self.commandlineAction = ["showEnvironmentTarget", self.configuration.pkgs_to_build] + else: + self.commandlineAction = ["showEnvironment", self.configuration.buildfile] + elif self.configuration.buildfile is not None: + self.commandlineAction = ["buildFile", self.configuration.buildfile, self.configuration.cmd] + elif self.configuration.revisions_changed: + self.commandlineAction = ["compareRevisions"] + elif self.configuration.show_versions: + self.commandlineAction = ["showVersions"] + elif self.configuration.parse_only: + self.commandlineAction = ["parseFiles"] + # FIXME - implement + #elif self.configuration.interactive: + # self.interactiveMode() + elif self.configuration.dot_graph: + if self.configuration.pkgs_to_build: + self.commandlineAction = ["generateDotGraph", self.configuration.pkgs_to_build, self.configuration.cmd] + else: + self.commandlineAction = None + bb.error("Please specify a package name for dependency graph generation.") + else: + if self.configuration.pkgs_to_build: + self.commandlineAction = ["buildTargets", self.configuration.pkgs_to_build, self.configuration.cmd] + else: + self.commandlineAction = None + bb.error("Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help' for usage information.") + + def runCommands(self, server, data, abort): + """ + Run any queued asynchronous command + This is done by the idle handler so it runs in true context rather than + tied to any UI. + """ + + return self.command.runAsyncCommand() def tryBuildPackage(self, fn, item, task, the_data): """ Build one task of a package, optionally build following task depends """ - bb.event.fire(bb.event.PkgStarted(item, the_data)) try: if not self.configuration.dry_run: bb.build.exec_task('do_%s' % task, the_data) - bb.event.fire(bb.event.PkgSucceeded(item, the_data)) return True except bb.build.FuncFailed: bb.msg.error(bb.msg.domain.Build, "task stack execution failed") - bb.event.fire(bb.event.PkgFailed(item, the_data)) raise except bb.build.EventException, e: event = e.args[1] bb.msg.error(bb.msg.domain.Build, "%s event exception, aborting" % bb.event.getName(event)) - bb.event.fire(bb.event.PkgFailed(item, the_data)) raise - def tryBuild(self, fn): + def tryBuild(self, fn, task): """ Build a provider and its dependencies. build_depends is a list of previous build dependencies (not runtime) If build_depends is empty, we're dealing with a runtime depends """ + the_data = self.bb_cache.loadDataFull(fn, self.configuration.data) item = self.status.pkg_fn[fn] @@ -133,9 +208,13 @@ class BBCooker: #if bb.build.stamp_is_current('do_%s' % self.configuration.cmd, the_data): # return True - return self.tryBuildPackage(fn, item, self.configuration.cmd, the_data) + return self.tryBuildPackage(fn, item, task, the_data) def showVersions(self): + + # Need files parsed + self.updateCache() + pkg_pn = self.status.pkg_pn preferred_versions = {} latest_versions = {} @@ -149,43 +228,36 @@ class BBCooker: pkg_list = pkg_pn.keys() pkg_list.sort() + bb.msg.plain("%-35s %25s %25s" % ("Package Name", "Latest Version", "Preferred Version")) + bb.msg.plain("%-35s %25s %25s\n" % ("============", "==============", "=================")) + for p in pkg_list: pref = preferred_versions[p] latest = latest_versions[p] - if pref != latest: - prefstr = pref[0][0] + ":" + pref[0][1] + '-' + pref[0][2] - else: + prefstr = pref[0][0] + ":" + pref[0][1] + '-' + pref[0][2] + lateststr = latest[0][0] + ":" + latest[0][1] + "-" + latest[0][2] + + if pref == latest: prefstr = "" - print "%-30s %20s %20s" % (p, latest[0][0] + ":" + latest[0][1] + "-" + latest[0][2], - prefstr) + bb.msg.plain("%-35s %25s %25s" % (p, lateststr, prefstr)) + def compareRevisions(self): + ret = bb.fetch.fetcher_compare_revisons(self.configuration.data) + bb.event.fire(bb.command.CookerCommandSetExitCode(ret), self.configuration.event_data) - def showEnvironment(self , buildfile = None, pkgs_to_build = []): + def showEnvironment(self, buildfile = None, pkgs_to_build = []): """ Show the outer or per-package environment """ fn = None envdata = None - if 'world' in pkgs_to_build: - print "'world' is not a valid target for --environment." - sys.exit(1) - - if len(pkgs_to_build) > 1: - print "Only one target can be used with the --environment option." - sys.exit(1) - if buildfile: - if len(pkgs_to_build) > 0: - print "No target should be used with the --environment and --buildfile options." - sys.exit(1) self.cb = None self.bb_cache = bb.cache.init(self) fn = self.matchFile(buildfile) - if not fn: - sys.exit(1) elif len(pkgs_to_build) == 1: self.updateCache() @@ -193,13 +265,9 @@ class BBCooker: bb.data.update_data(localdata) bb.data.expandKeys(localdata) - taskdata = bb.taskdata.TaskData(self.configuration.abort, self.configuration.tryaltconfigs) - - try: - taskdata.add_provider(localdata, self.status, pkgs_to_build[0]) - taskdata.add_unresolved(localdata, self.status) - except bb.providers.NoProvider: - sys.exit(1) + taskdata = bb.taskdata.TaskData(self.configuration.abort) + taskdata.add_provider(localdata, self.status, pkgs_to_build[0]) + taskdata.add_unresolved(localdata, self.status) targetid = taskdata.getbuild_id(pkgs_to_build[0]) fnid = taskdata.build_targets[targetid][0] @@ -211,55 +279,69 @@ class BBCooker: try: envdata = self.bb_cache.loadDataFull(fn, self.configuration.data) except IOError, e: - bb.msg.fatal(bb.msg.domain.Parsing, "Unable to read %s: %s" % (fn, e)) + bb.msg.error(bb.msg.domain.Parsing, "Unable to read %s: %s" % (fn, e)) + raise except Exception, e: - bb.msg.fatal(bb.msg.domain.Parsing, "%s" % e) + bb.msg.error(bb.msg.domain.Parsing, "%s" % e) + raise + + class dummywrite: + def __init__(self): + self.writebuf = "" + def write(self, output): + self.writebuf = self.writebuf + output # emit variables and shell functions try: - data.update_data( envdata ) - data.emit_env(sys.__stdout__, envdata, True) + data.update_data(envdata) + wb = dummywrite() + data.emit_env(wb, envdata, True) + bb.msg.plain(wb.writebuf) except Exception, e: bb.msg.fatal(bb.msg.domain.Parsing, "%s" % e) # emit the metadata which isnt valid shell - data.expandKeys( envdata ) + data.expandKeys(envdata) for e in envdata.keys(): if data.getVarFlag( e, 'python', envdata ): - sys.__stdout__.write("\npython %s () {\n%s}\n" % (e, data.getVar(e, envdata, 1))) + bb.msg.plain("\npython %s () {\n%s}\n" % (e, data.getVar(e, envdata, 1))) - def generateDotGraph( self, pkgs_to_build, ignore_deps ): + def generateDepTreeData(self, pkgs_to_build, task): """ - Generate a task dependency graph. - - pkgs_to_build A list of packages that needs to be built - ignore_deps A list of names where processing of dependencies - should be stopped. e.g. dependencies that get + Create a dependency tree of pkgs_to_build, returning the data. """ - for dep in ignore_deps: - self.status.ignored_dependencies.add(dep) + # Need files parsed + self.updateCache() + + # If we are told to do the None task then query the default task + if (task == None): + task = self.configuration.cmd + + pkgs_to_build = self.checkPackages(pkgs_to_build) localdata = data.createCopy(self.configuration.data) bb.data.update_data(localdata) bb.data.expandKeys(localdata) - taskdata = bb.taskdata.TaskData(self.configuration.abort, self.configuration.tryaltconfigs) + taskdata = bb.taskdata.TaskData(self.configuration.abort) runlist = [] - try: - for k in pkgs_to_build: - taskdata.add_provider(localdata, self.status, k) - runlist.append([k, "do_%s" % self.configuration.cmd]) - taskdata.add_unresolved(localdata, self.status) - except bb.providers.NoProvider: - sys.exit(1) + for k in pkgs_to_build: + taskdata.add_provider(localdata, self.status, k) + runlist.append([k, "do_%s" % task]) + taskdata.add_unresolved(localdata, self.status) + rq = bb.runqueue.RunQueue(self, self.configuration.data, self.status, taskdata, runlist) rq.prepare_runqueue() seen_fnids = [] - depends_file = file('depends.dot', 'w' ) - tdepends_file = file('task-depends.dot', 'w' ) - print >> depends_file, "digraph depends {" - print >> tdepends_file, "digraph depends {" + depend_tree = {} + depend_tree["depends"] = {} + depend_tree["tdepends"] = {} + depend_tree["pn"] = {} + depend_tree["rdepends-pn"] = {} + depend_tree["packages"] = {} + depend_tree["rdepends-pkg"] = {} + depend_tree["rrecs-pkg"] = {} for task in range(len(rq.runq_fnid)): taskname = rq.runq_task[task] @@ -267,43 +349,118 @@ class BBCooker: fn = taskdata.fn_index[fnid] pn = self.status.pkg_fn[fn] version = "%s:%s-%s" % self.status.pkg_pepvpr[fn] - print >> tdepends_file, '"%s.%s" [label="%s %s\\n%s\\n%s"]' % (pn, taskname, pn, taskname, version, fn) + if pn not in depend_tree["pn"]: + depend_tree["pn"][pn] = {} + depend_tree["pn"][pn]["filename"] = fn + depend_tree["pn"][pn]["version"] = version for dep in rq.runq_depends[task]: depfn = taskdata.fn_index[rq.runq_fnid[dep]] deppn = self.status.pkg_fn[depfn] - print >> tdepends_file, '"%s.%s" -> "%s.%s"' % (pn, rq.runq_task[task], deppn, rq.runq_task[dep]) + dotname = "%s.%s" % (pn, rq.runq_task[task]) + if not dotname in depend_tree["tdepends"]: + depend_tree["tdepends"][dotname] = [] + depend_tree["tdepends"][dotname].append("%s.%s" % (deppn, rq.runq_task[dep])) if fnid not in seen_fnids: seen_fnids.append(fnid) packages = [] - print >> depends_file, '"%s" [label="%s %s\\n%s"]' % (pn, pn, version, fn) - for depend in self.status.deps[fn]: - print >> depends_file, '"%s" -> "%s"' % (pn, depend) + + depend_tree["depends"][pn] = [] + for dep in taskdata.depids[fnid]: + depend_tree["depends"][pn].append(taskdata.build_names_index[dep]) + + depend_tree["rdepends-pn"][pn] = [] + for rdep in taskdata.rdepids[fnid]: + depend_tree["rdepends-pn"][pn].append(taskdata.run_names_index[rdep]) + rdepends = self.status.rundeps[fn] for package in rdepends: - for rdepend in re.findall("([\w.-]+)(\ \(.+\))?", rdepends[package]): - print >> depends_file, '"%s" -> "%s%s" [style=dashed]' % (package, rdepend[0], rdepend[1]) + depend_tree["rdepends-pkg"][package] = [] + for rdepend in rdepends[package]: + depend_tree["rdepends-pkg"][package].append(rdepend) packages.append(package) + rrecs = self.status.runrecs[fn] for package in rrecs: - for rdepend in re.findall("([\w.-]+)(\ \(.+\))?", rrecs[package]): - print >> depends_file, '"%s" -> "%s%s" [style=dashed]' % (package, rdepend[0], rdepend[1]) + depend_tree["rrecs-pkg"][package] = [] + for rdepend in rrecs[package]: + depend_tree["rrecs-pkg"][package].append(rdepend) if not package in packages: packages.append(package) + for package in packages: - if package != pn: - print >> depends_file, '"%s" [label="%s(%s) %s\\n%s"]' % (package, package, pn, version, fn) - for depend in self.status.deps[fn]: - print >> depends_file, '"%s" -> "%s"' % (package, depend) - # Prints a flattened form of the above where subpackages of a package are merged into the main pn - #print >> depends_file, '"%s" [label="%s %s\\n%s\\n%s"]' % (pn, pn, taskname, version, fn) - #for rdep in taskdata.rdepids[fnid]: - # print >> depends_file, '"%s" -> "%s" [style=dashed]' % (pn, taskdata.run_names_index[rdep]) - #for dep in taskdata.depids[fnid]: - # print >> depends_file, '"%s" -> "%s"' % (pn, taskdata.build_names_index[dep]) + if package not in depend_tree["packages"]: + depend_tree["packages"][package] = {} + depend_tree["packages"][package]["pn"] = pn + depend_tree["packages"][package]["filename"] = fn + depend_tree["packages"][package]["version"] = version + + return depend_tree + + + def generateDepTreeEvent(self, pkgs_to_build, task): + """ + Create a task dependency graph of pkgs_to_build. + Generate an event with the result + """ + depgraph = self.generateDepTreeData(pkgs_to_build, task) + bb.event.fire(bb.event.DepTreeGenerated(depgraph), self.configuration.data) + + def generateDotGraphFiles(self, pkgs_to_build, task): + """ + Create a task dependency graph of pkgs_to_build. + Save the result to a set of .dot files. + """ + + depgraph = self.generateDepTreeData(pkgs_to_build, task) + + # Prints a flattened form of package-depends below where subpackages of a package are merged into the main pn + depends_file = file('pn-depends.dot', 'w' ) + print >> depends_file, "digraph depends {" + for pn in depgraph["pn"]: + fn = depgraph["pn"][pn]["filename"] + version = depgraph["pn"][pn]["version"] + print >> depends_file, '"%s" [label="%s %s\\n%s"]' % (pn, pn, version, fn) + for pn in depgraph["depends"]: + for depend in depgraph["depends"][pn]: + print >> depends_file, '"%s" -> "%s"' % (pn, depend) + for pn in depgraph["rdepends-pn"]: + for rdepend in depgraph["rdepends-pn"][pn]: + print >> depends_file, '"%s" -> "%s" [style=dashed]' % (pn, rdepend) + print >> depends_file, "}" + bb.msg.plain("PN dependencies saved to 'pn-depends.dot'") + + depends_file = file('package-depends.dot', 'w' ) + print >> depends_file, "digraph depends {" + for package in depgraph["packages"]: + pn = depgraph["packages"][package]["pn"] + fn = depgraph["packages"][package]["filename"] + version = depgraph["packages"][package]["version"] + if package == pn: + print >> depends_file, '"%s" [label="%s %s\\n%s"]' % (pn, pn, version, fn) + else: + print >> depends_file, '"%s" [label="%s(%s) %s\\n%s"]' % (package, package, pn, version, fn) + for depend in depgraph["depends"][pn]: + print >> depends_file, '"%s" -> "%s"' % (package, depend) + for package in depgraph["rdepends-pkg"]: + for rdepend in depgraph["rdepends-pkg"][package]: + print >> depends_file, '"%s" -> "%s" [style=dashed]' % (package, rdepend) + for package in depgraph["rrecs-pkg"]: + for rdepend in depgraph["rrecs-pkg"][package]: + print >> depends_file, '"%s" -> "%s" [style=dashed]' % (package, rdepend) print >> depends_file, "}" + bb.msg.plain("Package dependencies saved to 'package-depends.dot'") + + tdepends_file = file('task-depends.dot', 'w' ) + print >> tdepends_file, "digraph depends {" + for task in depgraph["tdepends"]: + (pn, taskname) = task.rsplit(".", 1) + fn = depgraph["pn"][pn]["filename"] + version = depgraph["pn"][pn]["version"] + print >> tdepends_file, '"%s.%s" [label="%s %s\\n%s\\n%s"]' % (pn, taskname, pn, taskname, version, fn) + for dep in depgraph["tdepends"][task]: + print >> tdepends_file, '"%s" -> "%s"' % (task, dep) print >> tdepends_file, "}" - bb.msg.note(1, bb.msg.domain.Collection, "Dependencies saved to 'depends.dot'") - bb.msg.note(1, bb.msg.domain.Collection, "Task dependencies saved to 'task-depends.dot'") + bb.msg.plain("Task dependencies saved to 'task-depends.dot'") def buildDepgraph( self ): all_depends = self.status.all_depends @@ -324,7 +481,7 @@ class BBCooker: try: (providee, provider) = p.split(':') except: - bb.msg.error(bb.msg.domain.Provider, "Malformed option in PREFERRED_PROVIDERS variable: %s" % p) + bb.msg.fatal(bb.msg.domain.Provider, "Malformed option in PREFERRED_PROVIDERS variable: %s" % p) continue if providee in self.status.preferred and self.status.preferred[providee] != provider: bb.msg.error(bb.msg.domain.Provider, "conflicting preferences for %s: both %s and %s specified" % (providee, provider, self.status.preferred[providee])) @@ -362,19 +519,6 @@ class BBCooker: self.status.possible_world = None self.status.all_depends = None - def myProgressCallback( self, x, y, f, from_cache ): - """Update any tty with the progress change""" - if os.isatty(sys.stdout.fileno()): - sys.stdout.write("\rNOTE: Handling BitBake files: %s (%04d/%04d) [%2d %%]" % ( parsespin.next(), x, y, x*100/y ) ) - sys.stdout.flush() - else: - if x == 1: - sys.stdout.write("Parsing .bb files, please wait...") - sys.stdout.flush() - if x == y: - sys.stdout.write("done.") - sys.stdout.flush() - def interactiveMode( self ): """Drop off into a shell""" try: @@ -383,12 +527,10 @@ class BBCooker: bb.msg.fatal(bb.msg.domain.Parsing, "Sorry, shell not available (%s)" % details ) else: shell.start( self ) - sys.exit( 0 ) - def parseConfigurationFile( self, afiles ): + def parseConfigurationFile( self, afile ): try: - for afile in afiles: - self.configuration.data = bb.parse.handle( afile, self.configuration.data ) + self.configuration.data = bb.parse.handle( afile, self.configuration.data ) # Handle any INHERITs and inherit the base class inherits = ["base"] + (bb.data.getVar('INHERIT', self.configuration.data, True ) or "").split() @@ -402,10 +544,10 @@ class BBCooker: bb.fetch.fetcher_init(self.configuration.data) - bb.event.fire(bb.event.ConfigParsed(self.configuration.data)) + bb.event.fire(bb.event.ConfigParsed(), self.configuration.data) except IOError, e: - bb.msg.fatal(bb.msg.domain.Parsing, "IO Error: %s" % str(e) ) + bb.msg.fatal(bb.msg.domain.Parsing, "Error when parsing %s: %s" % (afile, str(e))) except bb.parse.ParseError, details: bb.msg.fatal(bb.msg.domain.Parsing, "Unable to parse %s (%s)" % (afile, details) ) @@ -439,17 +581,17 @@ class BBCooker: """ if not bb.data.getVar("BUILDNAME", self.configuration.data): bb.data.setVar("BUILDNAME", os.popen('date +%Y%m%d%H%M').readline().strip(), self.configuration.data) - bb.data.setVar("BUILDSTART", time.strftime('%m/%d/%Y %H:%M:%S',time.gmtime()),self.configuration.data) + bb.data.setVar("BUILDSTART", time.strftime('%m/%d/%Y %H:%M:%S',time.gmtime()), self.configuration.data) - def matchFile(self, buildfile): + def matchFiles(self, buildfile): """ - Convert the fragment buildfile into a real file - Error if there are too many matches + Find the .bb files which match the expression in 'buildfile'. """ + bf = os.path.abspath(buildfile) try: os.stat(bf) - return bf + return [bf] except OSError: (filelist, masked) = self.collect_bbfiles() regexp = re.compile(buildfile) @@ -458,27 +600,41 @@ class BBCooker: if regexp.search(f) and os.path.isfile(f): bf = f matches.append(f) - if len(matches) != 1: - bb.msg.error(bb.msg.domain.Parsing, "Unable to match %s (%s matches found):" % (buildfile, len(matches))) - for f in matches: - bb.msg.error(bb.msg.domain.Parsing, " %s" % f) - return False - return matches[0] + return matches - def buildFile(self, buildfile): + def matchFile(self, buildfile): + """ + Find the .bb file which matches the expression in 'buildfile'. + Raise an error if multiple files + """ + matches = self.matchFiles(buildfile) + if len(matches) != 1: + bb.msg.error(bb.msg.domain.Parsing, "Unable to match %s (%s matches found):" % (buildfile, len(matches))) + for f in matches: + bb.msg.error(bb.msg.domain.Parsing, " %s" % f) + raise MultipleMatches + return matches[0] + + def buildFile(self, buildfile, task): """ Build the file matching regexp buildfile """ - # Make sure our target is a fully qualified filename + # Parse the configuration here. We need to do it explicitly here since + # buildFile() doesn't use the cache + self.parseConfiguration() + + # If we are told to do the None task then query the default task + if (task == None): + task = self.configuration.cmd + fn = self.matchFile(buildfile) - if not fn: - return False + self.buildSetVars() # Load data into the cache for fn and parse the loaded cache data self.bb_cache = bb.cache.init(self) self.status = bb.cache.CacheData() - self.bb_cache.loadData(fn, self.configuration.data, self.status) + self.bb_cache.loadData(fn, self.configuration.data, self.status) # Tweak some variables item = self.bb_cache.getVar('PN', fn, True) @@ -493,159 +649,157 @@ class BBCooker: # Remove stamp for target if force mode active if self.configuration.force: - bb.msg.note(2, bb.msg.domain.RunQueue, "Remove stamp %s, %s" % (self.configuration.cmd, fn)) - bb.build.del_stamp('do_%s' % self.configuration.cmd, self.configuration.data) + bb.msg.note(2, bb.msg.domain.RunQueue, "Remove stamp %s, %s" % (task, fn)) + bb.build.del_stamp('do_%s' % task, self.status, fn) # Setup taskdata structure - taskdata = bb.taskdata.TaskData(self.configuration.abort, self.configuration.tryaltconfigs) + taskdata = bb.taskdata.TaskData(self.configuration.abort) taskdata.add_provider(self.configuration.data, self.status, item) buildname = bb.data.getVar("BUILDNAME", self.configuration.data) - bb.event.fire(bb.event.BuildStarted(buildname, [item], self.configuration.event_data)) + bb.event.fire(bb.event.BuildStarted(buildname, [item]), self.configuration.event_data) # Execute the runqueue - runlist = [[item, "do_%s" % self.configuration.cmd]] + runlist = [[item, "do_%s" % task]] + rq = bb.runqueue.RunQueue(self, self.configuration.data, self.status, taskdata, runlist) - rq.prepare_runqueue() - try: - failures = rq.execute_runqueue() - except runqueue.TaskFailure, fnids: + + def buildFileIdle(server, rq, abort): + + if abort or self.cookerAction == cookerStop: + rq.finish_runqueue(True) + elif self.cookerAction == cookerShutdown: + rq.finish_runqueue(False) failures = 0 - for fnid in fnids: - bb.msg.error(bb.msg.domain.Build, "'%s' failed" % taskdata.fn_index[fnid]) - failures = failures + 1 - bb.event.fire(bb.event.BuildCompleted(buildname, [item], self.configuration.event_data, failures)) - return False - bb.event.fire(bb.event.BuildCompleted(buildname, [item], self.configuration.event_data, failures)) - return True + try: + retval = rq.execute_runqueue() + except runqueue.TaskFailure, fnids: + for fnid in fnids: + bb.msg.error(bb.msg.domain.Build, "'%s' failed" % taskdata.fn_index[fnid]) + failures = failures + 1 + retval = False + if not retval: + self.command.finishAsyncCommand() + bb.event.fire(bb.event.BuildCompleted(buildname, item, failures), self.configuration.event_data) + return False + return 0.5 + + self.server.register_idle_function(buildFileIdle, rq) - def buildTargets(self, targets): + def buildTargets(self, targets, task): """ Attempt to build the targets specified """ - buildname = bb.data.getVar("BUILDNAME", self.configuration.data) - bb.event.fire(bb.event.BuildStarted(buildname, targets, self.configuration.event_data)) + # Need files parsed + self.updateCache() - localdata = data.createCopy(self.configuration.data) - bb.data.update_data(localdata) - bb.data.expandKeys(localdata) + # If we are told to do the NULL task then query the default task + if (task == None): + task = self.configuration.cmd - taskdata = bb.taskdata.TaskData(self.configuration.abort, self.configuration.tryaltconfigs) + targets = self.checkPackages(targets) - runlist = [] - try: - for k in targets: - taskdata.add_provider(localdata, self.status, k) - runlist.append([k, "do_%s" % self.configuration.cmd]) - taskdata.add_unresolved(localdata, self.status) - except bb.providers.NoProvider: - sys.exit(1) + def buildTargetsIdle(server, rq, abort): - rq = bb.runqueue.RunQueue(self, self.configuration.data, self.status, taskdata, runlist) - rq.prepare_runqueue() - try: - failures = rq.execute_runqueue() - except runqueue.TaskFailure, fnids: + if abort or self.cookerAction == cookerStop: + rq.finish_runqueue(True) + elif self.cookerAction == cookerShutdown: + rq.finish_runqueue(False) failures = 0 - for fnid in fnids: - bb.msg.error(bb.msg.domain.Build, "'%s' failed" % taskdata.fn_index[fnid]) - failures = failures + 1 - bb.event.fire(bb.event.BuildCompleted(buildname, targets, self.configuration.event_data, failures)) - sys.exit(1) - bb.event.fire(bb.event.BuildCompleted(buildname, targets, self.configuration.event_data, failures)) + try: + retval = rq.execute_runqueue() + except runqueue.TaskFailure, fnids: + for fnid in fnids: + bb.msg.error(bb.msg.domain.Build, "'%s' failed" % taskdata.fn_index[fnid]) + failures = failures + 1 + retval = False + if not retval: + self.command.finishAsyncCommand() + bb.event.fire(bb.event.BuildCompleted(buildname, targets, failures), self.configuration.event_data) + return None + return 0.5 - sys.exit(0) + self.buildSetVars() - def updateCache(self): - # Import Psyco if available and not disabled - import platform - if platform.machine() in ['i386', 'i486', 'i586', 'i686']: - if not self.configuration.disable_psyco: - try: - import psyco - except ImportError: - bb.msg.note(1, bb.msg.domain.Collection, "Psyco JIT Compiler (http://psyco.sf.net) not available. Install it to increase performance.") - else: - psyco.bind( self.parse_bbfiles ) - else: - bb.msg.note(1, bb.msg.domain.Collection, "You have disabled Psyco. This decreases performance.") + buildname = bb.data.getVar("BUILDNAME", self.configuration.data) + bb.event.fire(bb.event.BuildStarted(buildname, targets), self.configuration.event_data) - self.status = bb.cache.CacheData() + localdata = data.createCopy(self.configuration.data) + bb.data.update_data(localdata) + bb.data.expandKeys(localdata) - ignore = bb.data.getVar("ASSUME_PROVIDED", self.configuration.data, 1) or "" - self.status.ignored_dependencies = set( ignore.split() ) + taskdata = bb.taskdata.TaskData(self.configuration.abort) - self.handleCollections( bb.data.getVar("BBFILE_COLLECTIONS", self.configuration.data, 1) ) + runlist = [] + for k in targets: + taskdata.add_provider(localdata, self.status, k) + runlist.append([k, "do_%s" % task]) + taskdata.add_unresolved(localdata, self.status) - bb.msg.debug(1, bb.msg.domain.Collection, "collecting .bb files") - (filelist, masked) = self.collect_bbfiles() - bb.data.renameVar("__depends", "__base_depends", self.configuration.data) - self.parse_bbfiles(filelist, masked, self.myProgressCallback) - bb.msg.debug(1, bb.msg.domain.Collection, "parsing complete") + rq = bb.runqueue.RunQueue(self, self.configuration.data, self.status, taskdata, runlist) - self.buildDepgraph() + self.server.register_idle_function(buildTargetsIdle, rq) - def cook(self): - """ - We are building stuff here. We do the building - from here. By default we try to execute task - build. - """ + def updateCache(self): - # Wipe the OS environment - bb.utils.empty_environment() + if self.cookerState == cookerParsed: + return - if self.configuration.show_environment: - self.showEnvironment(self.configuration.buildfile, self.configuration.pkgs_to_build) - sys.exit( 0 ) + if self.cookerState != cookerParsing: - self.buildSetVars() + self.parseConfiguration () - if self.configuration.interactive: - self.interactiveMode() + # Import Psyco if available and not disabled + import platform + if platform.machine() in ['i386', 'i486', 'i586', 'i686']: + if not self.configuration.disable_psyco: + try: + import psyco + except ImportError: + bb.msg.note(1, bb.msg.domain.Collection, "Psyco JIT Compiler (http://psyco.sf.net) not available. Install it to increase performance.") + else: + psyco.bind( CookerParser.parse_next ) + else: + bb.msg.note(1, bb.msg.domain.Collection, "You have disabled Psyco. This decreases performance.") - if self.configuration.buildfile is not None: - if not self.buildFile(self.configuration.buildfile): - sys.exit(1) - sys.exit(0) + self.status = bb.cache.CacheData() - # initialise the parsing status now we know we will need deps - self.updateCache() + ignore = bb.data.getVar("ASSUME_PROVIDED", self.configuration.data, 1) or "" + self.status.ignored_dependencies = set(ignore.split()) + + for dep in self.configuration.extra_assume_provided: + self.status.ignored_dependencies.add(dep) + + self.handleCollections( bb.data.getVar("BBFILE_COLLECTIONS", self.configuration.data, 1) ) - if self.configuration.revisions_changed: - sys.exit(bb.fetch.fetcher_compare_revisons(self.configuration.data)) + bb.msg.debug(1, bb.msg.domain.Collection, "collecting .bb files") + (filelist, masked) = self.collect_bbfiles() + bb.data.renameVar("__depends", "__base_depends", self.configuration.data) - if self.configuration.parse_only: - bb.msg.note(1, bb.msg.domain.Collection, "Requested parsing .bb files only. Exiting.") - return 0 + self.parser = CookerParser(self, filelist, masked) + self.cookerState = cookerParsing - pkgs_to_build = self.configuration.pkgs_to_build + if not self.parser.parse_next(): + bb.msg.debug(1, bb.msg.domain.Collection, "parsing complete") + self.buildDepgraph() + self.cookerState = cookerParsed + return None - if len(pkgs_to_build) == 0 and not self.configuration.show_versions: - print "Nothing to do. Use 'bitbake world' to build everything, or run 'bitbake --help'" - print "for usage information." - sys.exit(0) + return True - try: - if self.configuration.show_versions: - self.showVersions() - sys.exit( 0 ) - if 'world' in pkgs_to_build: - self.buildWorldTargetList() - pkgs_to_build.remove('world') - for t in self.status.world_target: - pkgs_to_build.append(t) + def checkPackages(self, pkgs_to_build): - if self.configuration.dot_graph: - self.generateDotGraph( pkgs_to_build, self.configuration.ignored_dot_deps ) - sys.exit( 0 ) + if len(pkgs_to_build) == 0: + raise NothingToBuild - return self.buildTargets(pkgs_to_build) + if 'world' in pkgs_to_build: + self.buildWorldTargetList() + pkgs_to_build.remove('world') + for t in self.status.world_target: + pkgs_to_build.append(t) - except KeyboardInterrupt: - bb.msg.note(1, bb.msg.domain.Collection, "KeyboardInterrupt - Build not completed.") - sys.exit(1) + return pkgs_to_build def get_bbfiles( self, path = os.getcwd() ): """Get list of default .bb files by reading out the current directory""" @@ -717,59 +871,108 @@ class BBCooker: return (finalfiles, masked) - def parse_bbfiles(self, filelist, masked, progressCallback = None): - parsed, cached, skipped, error = 0, 0, 0, 0 - for i in xrange( len( filelist ) ): - f = filelist[i] + def serve(self): - #bb.msg.debug(1, bb.msg.domain.Collection, "parsing %s" % f) + # Empty the environment. The environment will be populated as + # necessary from the data store. + bb.utils.empty_environment() - # read a file's metadata + if self.configuration.profile: try: - fromCache, skip = self.bb_cache.loadData(f, self.configuration.data, self.status) - if skip: - skipped += 1 - bb.msg.debug(2, bb.msg.domain.Collection, "skipping %s" % f) - self.bb_cache.skip(f) - continue - elif fromCache: cached += 1 - else: parsed += 1 - - # Disabled by RP as was no longer functional - # allow metadata files to add items to BBFILES - #data.update_data(self.pkgdata[f]) - #addbbfiles = self.bb_cache.getVar('BBFILES', f, False) or None - #if addbbfiles: - # for aof in addbbfiles.split(): - # if not files.count(aof): - # if not os.path.isabs(aof): - # aof = os.path.join(os.path.dirname(f),aof) - # files.append(aof) - - # now inform the caller - if progressCallback is not None: - progressCallback( i + 1, len( filelist ), f, fromCache ) + import cProfile as profile + except: + import profile + + profile.runctx("self.server.serve_forever()", globals(), locals(), "profile.log") + + # Redirect stdout to capture profile information + pout = open('profile.log.processed', 'w') + so = sys.stdout.fileno() + os.dup2(pout.fileno(), so) + + import pstats + p = pstats.Stats('profile.log') + p.sort_stats('time') + p.print_stats() + p.print_callers() + p.sort_stats('cumulative') + p.print_stats() + + os.dup2(so, pout.fileno()) + pout.flush() + pout.close() + else: + self.server.serve_forever() + + bb.event.fire(CookerExit(), self.configuration.event_data) + +class CookerExit(bb.event.Event): + """ + Notify clients of the Cooker shutdown + """ + + def __init__(self): + bb.event.Event.__init__(self) + +class CookerParser: + def __init__(self, cooker, filelist, masked): + # Internal data + self.filelist = filelist + self.cooker = cooker + + # Accounting statistics + self.parsed = 0 + self.cached = 0 + self.error = 0 + self.masked = masked + self.total = len(filelist) + + self.skipped = 0 + self.virtuals = 0 + + # Pointer to the next file to parse + self.pointer = 0 + + def parse_next(self): + if self.pointer < len(self.filelist): + f = self.filelist[self.pointer] + cooker = self.cooker + + try: + fromCache, skipped, virtuals = cooker.bb_cache.loadData(f, cooker.configuration.data, cooker.status) + if fromCache: + self.cached += 1 + else: + self.parsed += 1 + + self.skipped += skipped + self.virtuals += virtuals except IOError, e: - self.bb_cache.remove(f) + self.error += 1 + cooker.bb_cache.remove(f) bb.msg.error(bb.msg.domain.Collection, "opening %s: %s" % (f, e)) pass except KeyboardInterrupt: - self.bb_cache.sync() + cooker.bb_cache.remove(f) + cooker.bb_cache.sync() raise except Exception, e: - error += 1 - self.bb_cache.remove(f) + self.error += 1 + cooker.bb_cache.remove(f) bb.msg.error(bb.msg.domain.Collection, "%s while parsing %s" % (e, f)) except: - self.bb_cache.remove(f) + cooker.bb_cache.remove(f) raise + finally: + bb.event.fire(bb.event.ParseProgress(self.cached, self.parsed, self.skipped, self.masked, self.virtuals, self.error, self.total), cooker.configuration.event_data) - if progressCallback is not None: - print "\r" # need newline after Handling Bitbake files message - bb.msg.note(1, bb.msg.domain.Collection, "Parsing finished. %d cached, %d parsed, %d skipped, %d masked." % ( cached, parsed, skipped, masked )) + self.pointer += 1 - self.bb_cache.sync() + if self.pointer >= self.total: + cooker.bb_cache.sync() + if self.error > 0: + raise ParsingErrorsFound + return False + return True - if error > 0: - bb.msg.fatal(bb.msg.domain.Collection, "Parsing errors found, exiting...") diff --git a/bitbake/lib/bb/daemonize.py b/bitbake/lib/bb/daemonize.py new file mode 100644 index 0000000000..1a8bb379f4 --- /dev/null +++ b/bitbake/lib/bb/daemonize.py @@ -0,0 +1,191 @@ +""" +Python Deamonizing helper + +Configurable daemon behaviors: + + 1.) The current working directory set to the "/" directory. + 2.) The current file creation mode mask set to 0. + 3.) Close all open files (1024). + 4.) Redirect standard I/O streams to "/dev/null". + +A failed call to fork() now raises an exception. + +References: + 1) Advanced Programming in the Unix Environment: W. Richard Stevens + 2) Unix Programming Frequently Asked Questions: + http://www.erlenstar.demon.co.uk/unix/faq_toc.html + +Modified to allow a function to be daemonized and return for +bitbake use by Richard Purdie +""" + +__author__ = "Chad J. Schroeder" +__copyright__ = "Copyright (C) 2005 Chad J. Schroeder" +__version__ = "0.2" + +# Standard Python modules. +import os # Miscellaneous OS interfaces. +import sys # System-specific parameters and functions. + +# Default daemon parameters. +# File mode creation mask of the daemon. +# For BitBake's children, we do want to inherit the parent umask. +UMASK = None + +# Default maximum for the number of available file descriptors. +MAXFD = 1024 + +# The standard I/O file descriptors are redirected to /dev/null by default. +if (hasattr(os, "devnull")): + REDIRECT_TO = os.devnull +else: + REDIRECT_TO = "/dev/null" + +def createDaemon(function, logfile): + """ + Detach a process from the controlling terminal and run it in the + background as a daemon, returning control to the caller. + """ + + try: + # Fork a child process so the parent can exit. This returns control to + # the command-line or shell. It also guarantees that the child will not + # be a process group leader, since the child receives a new process ID + # and inherits the parent's process group ID. This step is required + # to insure that the next call to os.setsid is successful. + pid = os.fork() + except OSError, e: + raise Exception, "%s [%d]" % (e.strerror, e.errno) + + if (pid == 0): # The first child. + # To become the session leader of this new session and the process group + # leader of the new process group, we call os.setsid(). The process is + # also guaranteed not to have a controlling terminal. + os.setsid() + + # Is ignoring SIGHUP necessary? + # + # It's often suggested that the SIGHUP signal should be ignored before + # the second fork to avoid premature termination of the process. The + # reason is that when the first child terminates, all processes, e.g. + # the second child, in the orphaned group will be sent a SIGHUP. + # + # "However, as part of the session management system, there are exactly + # two cases where SIGHUP is sent on the death of a process: + # + # 1) When the process that dies is the session leader of a session that + # is attached to a terminal device, SIGHUP is sent to all processes + # in the foreground process group of that terminal device. + # 2) When the death of a process causes a process group to become + # orphaned, and one or more processes in the orphaned group are + # stopped, then SIGHUP and SIGCONT are sent to all members of the + # orphaned group." [2] + # + # The first case can be ignored since the child is guaranteed not to have + # a controlling terminal. The second case isn't so easy to dismiss. + # The process group is orphaned when the first child terminates and + # POSIX.1 requires that every STOPPED process in an orphaned process + # group be sent a SIGHUP signal followed by a SIGCONT signal. Since the + # second child is not STOPPED though, we can safely forego ignoring the + # SIGHUP signal. In any case, there are no ill-effects if it is ignored. + # + # import signal # Set handlers for asynchronous events. + # signal.signal(signal.SIGHUP, signal.SIG_IGN) + + try: + # Fork a second child and exit immediately to prevent zombies. This + # causes the second child process to be orphaned, making the init + # process responsible for its cleanup. And, since the first child is + # a session leader without a controlling terminal, it's possible for + # it to acquire one by opening a terminal in the future (System V- + # based systems). This second fork guarantees that the child is no + # longer a session leader, preventing the daemon from ever acquiring + # a controlling terminal. + pid = os.fork() # Fork a second child. + except OSError, e: + raise Exception, "%s [%d]" % (e.strerror, e.errno) + + if (pid == 0): # The second child. + # We probably don't want the file mode creation mask inherited from + # the parent, so we give the child complete control over permissions. + if UMASK is not None: + os.umask(UMASK) + else: + # Parent (the first child) of the second child. + os._exit(0) + else: + # exit() or _exit()? + # _exit is like exit(), but it doesn't call any functions registered + # with atexit (and on_exit) or any registered signal handlers. It also + # closes any open file descriptors. Using exit() may cause all stdio + # streams to be flushed twice and any temporary files may be unexpectedly + # removed. It's therefore recommended that child branches of a fork() + # and the parent branch(es) of a daemon use _exit(). + return + + # Close all open file descriptors. This prevents the child from keeping + # open any file descriptors inherited from the parent. There is a variety + # of methods to accomplish this task. Three are listed below. + # + # Try the system configuration variable, SC_OPEN_MAX, to obtain the maximum + # number of open file descriptors to close. If it doesn't exists, use + # the default value (configurable). + # + # try: + # maxfd = os.sysconf("SC_OPEN_MAX") + # except (AttributeError, ValueError): + # maxfd = MAXFD + # + # OR + # + # if (os.sysconf_names.has_key("SC_OPEN_MAX")): + # maxfd = os.sysconf("SC_OPEN_MAX") + # else: + # maxfd = MAXFD + # + # OR + # + # Use the getrlimit method to retrieve the maximum file descriptor number + # that can be opened by this process. If there is not limit on the + # resource, use the default value. + # + import resource # Resource usage information. + maxfd = resource.getrlimit(resource.RLIMIT_NOFILE)[1] + if (maxfd == resource.RLIM_INFINITY): + maxfd = MAXFD + + # Iterate through and close all file descriptors. +# for fd in range(0, maxfd): +# try: +# os.close(fd) +# except OSError: # ERROR, fd wasn't open to begin with (ignored) +# pass + + # Redirect the standard I/O file descriptors to the specified file. Since + # the daemon has no controlling terminal, most daemons redirect stdin, + # stdout, and stderr to /dev/null. This is done to prevent side-effects + # from reads and writes to the standard I/O file descriptors. + + # This call to open is guaranteed to return the lowest file descriptor, + # which will be 0 (stdin), since it was closed above. +# os.open(REDIRECT_TO, os.O_RDWR) # standard input (0) + + # Duplicate standard input to standard output and standard error. +# os.dup2(0, 1) # standard output (1) +# os.dup2(0, 2) # standard error (2) + + + si = file('/dev/null', 'r') + so = file(logfile, 'w') + se = so + + + # Replace those fds with our own + os.dup2(si.fileno(), sys.stdin.fileno()) + os.dup2(so.fileno(), sys.stdout.fileno()) + os.dup2(se.fileno(), sys.stderr.fileno()) + + function() + + os._exit(0) + diff --git a/bitbake/lib/bb/data.py b/bitbake/lib/bb/data.py index f424ac7a22..d3058b9a1d 100644 --- a/bitbake/lib/bb/data.py +++ b/bitbake/lib/bb/data.py @@ -37,7 +37,7 @@ the speed is more critical here. # #Based on functions from the base bb module, Copyright 2003 Holger Schurig -import sys, os, re, time, types +import sys, os, re, types if sys.argv[0][-5:] == "pydoc": path = os.path.dirname(os.path.dirname(sys.argv[1])) else: diff --git a/bitbake/lib/bb/event.py b/bitbake/lib/bb/event.py index 9d7341f878..7251d78715 100644 --- a/bitbake/lib/bb/event.py +++ b/bitbake/lib/bb/event.py @@ -24,21 +24,18 @@ BitBake build tools. import os, re import bb.utils +import pickle + +# This is the pid for which we should generate the event. This is set when +# the runqueue forks off. +worker_pid = 0 +worker_pipe = None class Event: """Base class for events""" - type = "Event" - - def __init__(self, d): - self._data = d - - def getData(self): - return self._data - - def setData(self, data): - self._data = data - data = property(getData, setData, None, "data property") + def __init__(self): + self.pid = worker_pid NotHandled = 0 Handled = 1 @@ -47,75 +44,83 @@ Registered = 10 AlreadyRegistered = 14 # Internal -_handlers = [] -_handlers_dict = {} +_handlers = {} +_ui_handlers = {} +_ui_handler_seq = 0 -def tmpHandler(event): - """Default handler for code events""" - return NotHandled +def fire(event, d): + """Fire off an Event""" -def defaultTmpHandler(): - tmp = "def tmpHandler(e):\n\t\"\"\"heh\"\"\"\n\treturn NotHandled" - comp = bb.utils.better_compile(tmp, "tmpHandler(e)", "bb.event.defaultTmpHandler") - return comp + if worker_pid != 0: + worker_fire(event, d) + return -def fire(event): - """Fire off an Event""" - for h in _handlers: + for handler in _handlers: + h = _handlers[handler] + event.data = d if type(h).__name__ == "code": exec(h) - if tmpHandler(event) == Handled: - return Handled + tmpHandler(event) else: - if h(event) == Handled: - return Handled - return NotHandled + h(event) + del event.data + + errors = [] + for h in _ui_handlers: + #print "Sending event %s" % event + try: + # We use pickle here since it better handles object instances + # which xmlrpc's marshaller does not. Events *must* be serializable + # by pickle. + _ui_handlers[h].event.send((pickle.dumps(event))) + except: + errors.append(h) + for h in errors: + del _ui_handlers[h] + +def worker_fire(event, d): + data = "" + pickle.dumps(event) + "" + if os.write(worker_pipe, data) != len (data): + print "Error sending event to server (short write)" + +def fire_from_worker(event, d): + if not event.startswith("") or not event.endswith(""): + print "Error, not an event" + return + event = pickle.loads(event[7:-8]) + bb.event.fire(event, d) def register(name, handler): """Register an Event handler""" # already registered - if name in _handlers_dict: + if name in _handlers: return AlreadyRegistered if handler is not None: -# handle string containing python code + # handle string containing python code if type(handler).__name__ == "str": - _registerCode(handler) + tmp = "def tmpHandler(e):\n%s" % handler + comp = bb.utils.better_compile(tmp, "tmpHandler(e)", "bb.event._registerCode") + _handlers[name] = comp else: - _handlers.append(handler) + _handlers[name] = handler - _handlers_dict[name] = 1 return Registered -def _registerCode(handlerStr): - """Register a 'code' Event. - Deprecated interface; call register instead. - - Expects to be passed python code as a string, which will - be passed in turn to compile() and then exec(). Note that - the code will be within a function, so should have had - appropriate tabbing put in place.""" - tmp = "def tmpHandler(e):\n%s" % handlerStr - comp = bb.utils.better_compile(tmp, "tmpHandler(e)", "bb.event._registerCode") -# prevent duplicate registration - _handlers.append(comp) - def remove(name, handler): """Remove an Event handler""" + _handlers.pop(name) - _handlers_dict.pop(name) - if type(handler).__name__ == "str": - return _removeCode(handler) - else: - _handlers.remove(handler) +def register_UIHhandler(handler): + bb.event._ui_handler_seq = bb.event._ui_handler_seq + 1 + _ui_handlers[_ui_handler_seq] = handler + return _ui_handler_seq -def _removeCode(handlerStr): - """Remove a 'code' Event handler - Deprecated interface; call remove instead.""" - tmp = "def tmpHandler(e):\n%s" % handlerStr - comp = bb.utils.better_compile(tmp, "tmpHandler(e)", "bb.event._removeCode") - _handlers.remove(comp) +def unregister_UIHhandler(handlerNum): + if handlerNum in _ui_handlers: + del _ui_handlers[handlerNum] + return def getName(e): """Returns the name of a class or class instance""" @@ -130,17 +135,17 @@ class ConfigParsed(Event): class RecipeParsed(Event): """ Recipe Parsing Complete """ - def __init__(self, fn, d): + def __init__(self, fn): self.fn = fn - Event.__init__(self, d) + Event.__init__(self) class StampUpdate(Event): """Trigger for any adjustment of the stamp files to happen""" - def __init__(self, targets, stampfns, d): + def __init__(self, targets, stampfns): self._targets = targets self._stampfns = stampfns - Event.__init__(self, d) + Event.__init__(self) def getStampPrefix(self): return self._stampfns @@ -151,29 +156,13 @@ class StampUpdate(Event): stampPrefix = property(getStampPrefix) targets = property(getTargets) -class PkgBase(Event): - """Base class for package events""" - - def __init__(self, t, d): - self._pkg = t - Event.__init__(self, d) - - def getPkg(self): - return self._pkg - - def setPkg(self, pkg): - self._pkg = pkg - - pkg = property(getPkg, setPkg, None, "pkg property") - - class BuildBase(Event): """Base class for bbmake run events""" - def __init__(self, n, p, c, failures = 0): + def __init__(self, n, p, failures = 0): self._name = n self._pkgs = p - Event.__init__(self, c) + Event.__init__(self) self._failures = failures def getPkgs(self): @@ -205,33 +194,8 @@ class BuildBase(Event): cfg = property(getCfg, setCfg, None, "cfg property") -class DepBase(PkgBase): - """Base class for dependency events""" - - def __init__(self, t, data, d): - self._dep = d - PkgBase.__init__(self, t, data) - - def getDep(self): - return self._dep - - def setDep(self, dep): - self._dep = dep - - dep = property(getDep, setDep, None, "dep property") - - -class PkgStarted(PkgBase): - """Package build started""" -class PkgFailed(PkgBase): - """Package build failed""" - - -class PkgSucceeded(PkgBase): - """Package build completed""" - class BuildStarted(BuildBase): """bbmake build run started""" @@ -241,18 +205,13 @@ class BuildCompleted(BuildBase): """bbmake build run completed""" -class UnsatisfiedDep(DepBase): - """Unsatisfied Dependency""" -class RecursiveDep(DepBase): - """Recursive Dependency""" - class NoProvider(Event): """No Provider for an Event""" - def __init__(self, item, data,runtime=False): - Event.__init__(self, data) + def __init__(self, item, runtime=False): + Event.__init__(self) self._item = item self._runtime = runtime @@ -265,8 +224,8 @@ class NoProvider(Event): class MultipleProviders(Event): """Multiple Providers""" - def __init__(self, item, candidates, data, runtime = False): - Event.__init__(self, data) + def __init__(self, item, candidates, runtime = False): + Event.__init__(self) self._item = item self._candidates = candidates self._is_runtime = runtime @@ -288,3 +247,29 @@ class MultipleProviders(Event): Get the possible Candidates for a PROVIDER. """ return self._candidates + +class ParseProgress(Event): + """ + Parsing Progress Event + """ + + def __init__(self, cached, parsed, skipped, masked, virtuals, errors, total): + Event.__init__(self) + self.cached = cached + self.parsed = parsed + self.skipped = skipped + self.virtuals = virtuals + self.masked = masked + self.errors = errors + self.sofar = cached + parsed + self.total = total + +class DepTreeGenerated(Event): + """ + Event when a dependency tree has been generated + """ + + def __init__(self, depgraph): + Event.__init__(self) + self._depgraph = depgraph + diff --git a/bitbake/lib/bb/fetch/__init__.py b/bitbake/lib/bb/fetch/__init__.py index 7326ed0f46..ab4658bc3b 100644 --- a/bitbake/lib/bb/fetch/__init__.py +++ b/bitbake/lib/bb/fetch/__init__.py @@ -99,6 +99,11 @@ def fetcher_init(d): pd.delDomain("BB_URI_HEADREVS") else: bb.msg.fatal(bb.msg.domain.Fetcher, "Invalid SRCREV cache policy of: %s" % srcrev_policy) + + for m in methods: + if hasattr(m, "init"): + m.init(d) + # Make sure our domains exist pd.addDomain("BB_URI_HEADREVS") pd.addDomain("BB_URI_LOCALCOUNT") @@ -467,6 +472,23 @@ class Fetch(object): srcrev_internal_helper = staticmethod(srcrev_internal_helper) + def localcount_internal_helper(ud, d): + """ + Return: + a) a locked localcount if specified + b) None otherwise + """ + + localcount= None + if 'name' in ud.parm: + pn = data.getVar("PN", d, 1) + localcount = data.getVar("LOCALCOUNT_" + ud.parm['name'], d, 1) + if not localcount: + localcount = data.getVar("LOCALCOUNT", d, 1) + return localcount + + localcount_internal_helper = staticmethod(localcount_internal_helper) + def try_mirror(d, tarfn): """ Try to use a mirrored version of the sources. We do this @@ -555,12 +577,7 @@ class Fetch(object): """ """ - has_sortable_valid = hasattr(self, "_sortable_revision_valid") - has_sortable = hasattr(self, "_sortable_revision") - - if has_sortable and not has_sortable_valid: - return self._sortable_revision(url, ud, d) - elif has_sortable and self._sortable_revision_valid(url, ud, d): + if hasattr(self, "_sortable_revision"): return self._sortable_revision(url, ud, d) pd = persist_data.PersistData(d) @@ -568,13 +585,24 @@ class Fetch(object): latest_rev = self._build_revision(url, ud, d) last_rev = pd.getValue("BB_URI_LOCALCOUNT", key + "_rev") - count = pd.getValue("BB_URI_LOCALCOUNT", key + "_count") + uselocalcount = bb.data.getVar("BB_LOCALCOUNT_OVERRIDE", d, True) or False + count = None + if uselocalcount: + count = Fetch.localcount_internal_helper(ud, d) + if count is None: + count = pd.getValue("BB_URI_LOCALCOUNT", key + "_count") if last_rev == latest_rev: return str(count + "+" + latest_rev) + buildindex_provided = hasattr(self, "_sortable_buildindex") + if buildindex_provided: + count = self._sortable_buildindex(url, ud, d, latest_rev) + if count is None: count = "0" + elif uselocalcount or buildindex_provided: + count = str(count) else: count = str(int(count) + 1) diff --git a/bitbake/lib/bb/fetch/cvs.py b/bitbake/lib/bb/fetch/cvs.py index d8bd4eaf75..90a006500e 100644 --- a/bitbake/lib/bb/fetch/cvs.py +++ b/bitbake/lib/bb/fetch/cvs.py @@ -41,7 +41,7 @@ class Cvs(Fetch): """ Check to see if a given url can be fetched with cvs. """ - return ud.type in ['cvs', 'pserver'] + return ud.type in ['cvs'] def localpath(self, url, ud, d): if not "module" in ud.parm: diff --git a/bitbake/lib/bb/fetch/git.py b/bitbake/lib/bb/fetch/git.py index 3016f0f00d..0e68325db9 100644 --- a/bitbake/lib/bb/fetch/git.py +++ b/bitbake/lib/bb/fetch/git.py @@ -28,6 +28,12 @@ from bb.fetch import runfetchcmd class Git(Fetch): """Class to fetch a module or modules from git repositories""" + def init(self, d): + # + # Only enable _sortable revision if the key is set + # + if bb.data.getVar("BB_GIT_CLONE_FOR_SRCREV", d, True): + self._sortable_buildindex = self._sortable_buildindex_disabled def supports(self, url, ud, d): """ Check to see if a given url can be fetched with git. @@ -58,10 +64,18 @@ class Git(Fetch): if not ud.tag or ud.tag == "master": ud.tag = self.latest_revision(url, ud, d) + subdir = ud.parm.get("subpath", "") + if subdir != "": + if subdir.endswith("/"): + subdir = subdir[:-1] + subdirpath = os.path.join(ud.path, subdir); + else: + subdirpath = ud.path; + if 'fullclone' in ud.parm: ud.localfile = ud.mirrortarball else: - ud.localfile = data.expand('git_%s%s_%s.tar.gz' % (ud.host, ud.path.replace('/', '.'), ud.tag), d) + ud.localfile = data.expand('git_%s%s_%s.tar.gz' % (ud.host, subdirpath.replace('/', '.'), ud.tag), d) return os.path.join(data.getVar("DL_DIR", d, True), ud.localfile) @@ -111,10 +125,27 @@ class Git(Fetch): if os.path.exists(codir): bb.utils.prunedir(codir) + subdir = ud.parm.get("subpath", "") + if subdir != "": + if subdir.endswith("/"): + subdirbase = os.path.basename(subdir[:-1]) + else: + subdirbase = os.path.basename(subdir) + else: + subdirbase = "" + + if subdir != "": + readpathspec = ":%s" % (subdir) + codir = os.path.join(codir, "git") + coprefix = os.path.join(codir, subdirbase, "") + else: + readpathspec = "" + coprefix = os.path.join(codir, "git", "") + bb.mkdirhier(codir) os.chdir(ud.clonedir) - runfetchcmd("git read-tree %s" % (ud.tag), d) - runfetchcmd("git checkout-index -q -f --prefix=%s -a" % (os.path.join(codir, "git", "")), d) + runfetchcmd("git read-tree %s%s" % (ud.tag, readpathspec), d) + runfetchcmd("git checkout-index -q -f --prefix=%s -a" % (coprefix), d) os.chdir(codir) bb.msg.note(1, bb.msg.domain.Fetcher, "Creating tarball of git checkout") @@ -154,42 +185,32 @@ class Git(Fetch): def _build_revision(self, url, ud, d): return ud.tag - def _sortable_revision_valid(self, url, ud, d): - return bb.data.getVar("BB_GIT_CLONE_FOR_SRCREV", d, True) or False - - def _sortable_revision(self, url, ud, d): + def _sortable_buildindex_disabled(self, url, ud, d, rev): """ - This is only called when _sortable_revision_valid called true - - We will have to get the updated revision. + Return a suitable buildindex for the revision specified. This is done by counting revisions + using "git rev-list" which may or may not work in different circumstances. """ - key = "GIT_CACHED_REVISION-%s-%s" % (gitsrcname, ud.tag) - if bb.data.getVar(key, d): - return bb.data.getVar(key, d) - - - # Runtime warning on wrongly configured sources - if ud.tag == "1": - bb.msg.error(1, bb.msg.domain.Fetcher, "SRCREV is '1'. This indicates a configuration error of %s" % url) - return "0+1" - cwd = os.getcwd() # Check if we have the rev already + if not os.path.exists(ud.clonedir): print "no repo" self.go(None, ud, d) + if not os.path.exists(ud.clonedir): + bb.msg.error(bb.msg.domain.Fetcher, "GIT repository for %s doesn't exist in %s, cannot get sortable buildnumber, using old value" % (url, ud.clonedir)) + return None + os.chdir(ud.clonedir) - if not self._contains_ref(ud.tag, d): + if not self._contains_ref(rev, d): self.go(None, ud, d) - output = runfetchcmd("git rev-list %s -- 2> /dev/null | wc -l" % ud.tag, d, quiet=True) + output = runfetchcmd("git rev-list %s -- 2> /dev/null | wc -l" % rev, d, quiet=True) os.chdir(cwd) - sortable_revision = "%s+%s" % (output.split()[0], ud.tag) - bb.data.setVar(key, sortable_revision, d) - return sortable_revision - + buildindex = "%s" % output.split()[0] + bb.msg.debug(1, bb.msg.domain.Fetcher, "GIT repository for %s in %s is returning %s revisions in rev-list before %s" % (url, repodir, buildindex, rev)) + return buildindex diff --git a/bitbake/lib/bb/fetch/local.py b/bitbake/lib/bb/fetch/local.py index 577774e597..f9bdf589cb 100644 --- a/bitbake/lib/bb/fetch/local.py +++ b/bitbake/lib/bb/fetch/local.py @@ -33,9 +33,9 @@ from bb.fetch import Fetch class Local(Fetch): def supports(self, url, urldata, d): """ - Check to see if a given url can be fetched with cvs. + Check to see if a given url represents a local fetch. """ - return urldata.type in ['file','patch'] + return urldata.type in ['file'] def localpath(self, url, urldata, d): """ diff --git a/bitbake/lib/bb/fetch/svk.py b/bitbake/lib/bb/fetch/svk.py index 442f85804f..120dad9d4e 100644 --- a/bitbake/lib/bb/fetch/svk.py +++ b/bitbake/lib/bb/fetch/svk.py @@ -36,7 +36,7 @@ class Svk(Fetch): """Class to fetch a module or modules from svk repositories""" def supports(self, url, ud, d): """ - Check to see if a given url can be fetched with cvs. + Check to see if a given url can be fetched with svk. """ return ud.type in ['svk'] diff --git a/bitbake/lib/bb/fetch/wget.py b/bitbake/lib/bb/fetch/wget.py index a0dca94040..fd93c7ec46 100644 --- a/bitbake/lib/bb/fetch/wget.py +++ b/bitbake/lib/bb/fetch/wget.py @@ -36,7 +36,7 @@ class Wget(Fetch): """Class to fetch urls via 'wget'""" def supports(self, url, ud, d): """ - Check to see if a given url can be fetched with cvs. + Check to see if a given url can be fetched with wget. """ return ud.type in ['http','https','ftp'] diff --git a/bitbake/lib/bb/msg.py b/bitbake/lib/bb/msg.py index a1b31e5d60..3fcf7091be 100644 --- a/bitbake/lib/bb/msg.py +++ b/bitbake/lib/bb/msg.py @@ -22,8 +22,8 @@ Message handling infrastructure for bitbake # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import sys, os, re, bb -from bb import utils, event +import sys, bb +from bb import event debug_level = {} @@ -47,9 +47,9 @@ domain = bb.utils.Enum( class MsgBase(bb.event.Event): """Base class for messages""" - def __init__(self, msg, d ): + def __init__(self, msg): self._message = msg - event.Event.__init__(self, d) + event.Event.__init__(self) class MsgDebug(MsgBase): """Debug Message""" @@ -97,33 +97,29 @@ def set_debug_domains(domains): # def debug(level, domain, msg, fn = None): - bb.event.fire(MsgDebug(msg, None)) if not domain: domain = 'default' if debug_level[domain] >= level: - print 'DEBUG: ' + msg + bb.event.fire(MsgDebug(msg), None) def note(level, domain, msg, fn = None): - bb.event.fire(MsgNote(msg, None)) if not domain: domain = 'default' if level == 1 or verbose or debug_level[domain] >= 1: - print 'NOTE: ' + msg + bb.event.fire(MsgNote(msg), None) def warn(domain, msg, fn = None): - bb.event.fire(MsgWarn(msg, None)) - print 'WARNING: ' + msg + bb.event.fire(MsgWarn(msg), None) def error(domain, msg, fn = None): - bb.event.fire(MsgError(msg, None)) + bb.event.fire(MsgError(msg), None) print 'ERROR: ' + msg def fatal(domain, msg, fn = None): - bb.event.fire(MsgFatal(msg, None)) - print 'ERROR: ' + msg + bb.event.fire(MsgFatal(msg), None) + print 'FATAL: ' + msg sys.exit(1) def plain(msg, fn = None): - bb.event.fire(MsgPlain(msg, None)) - print msg + bb.event.fire(MsgPlain(msg), None) diff --git a/bitbake/lib/bb/parse/parse_py/BBHandler.py b/bitbake/lib/bb/parse/parse_py/BBHandler.py index 915db214f5..86fa18ebd2 100644 --- a/bitbake/lib/bb/parse/parse_py/BBHandler.py +++ b/bitbake/lib/bb/parse/parse_py/BBHandler.py @@ -94,7 +94,7 @@ def finalise(fn, d): for f in anonfuncs: code = code + " %s(d)\n" % f data.setVar("__anonfunc", code, d) - build.exec_func_python("__anonfunc", d) + build.exec_func("__anonfunc", d) data.delVar('T', d) if t: data.setVar('T', t, d) @@ -114,7 +114,7 @@ def finalise(fn, d): tasklist = data.getVar('__BBTASKS', d) or [] bb.build.add_tasks(tasklist, d) - bb.event.fire(bb.event.RecipeParsed(fn, d)) + bb.event.fire(bb.event.RecipeParsed(fn), d) def handle(fn, d, include = 0): @@ -185,18 +185,26 @@ def handle(fn, d, include = 0): multi = data.getVar('BBCLASSEXTEND', d, 1) if multi: based = bb.data.createCopy(d) + else: + based = d + try: finalise(fn, based) - darray = {"": based} - for cls in multi.split(): - pn = data.getVar('PN', d, True) - based = bb.data.createCopy(d) - data.setVar('PN', pn + '-' + cls, based) - inherit([cls], based) + except bb.parse.SkipPackage: + bb.data.setVar("__SKIPPED", True, based) + darray = {"": based} + + for cls in (multi or "").split(): + pn = data.getVar('PN', d, True) + based = bb.data.createCopy(d) + data.setVar('PN', pn + '-' + cls, based) + inherit([cls], based) + try: finalise(fn, based) - darray[cls] = based - return darray - else: - finalise(fn, d) + except bb.parse.SkipPackage: + bb.data.setVar("__SKIPPED", True, based) + darray[cls] = based + return darray + bbpath.pop(0) if oldfile: bb.data.setVar("FILE", oldfile, d) diff --git a/bitbake/lib/bb/parse/parse_py/ConfHandler.py b/bitbake/lib/bb/parse/parse_py/ConfHandler.py index c9f1ea13fb..23316ada58 100644 --- a/bitbake/lib/bb/parse/parse_py/ConfHandler.py +++ b/bitbake/lib/bb/parse/parse_py/ConfHandler.py @@ -34,10 +34,17 @@ __require_regexp__ = re.compile( r"require\s+(.+)" ) __export_regexp__ = re.compile( r"export\s+(.+)" ) def init(data): - if not bb.data.getVar('TOPDIR', data): - bb.data.setVar('TOPDIR', os.getcwd(), data) + topdir = bb.data.getVar('TOPDIR', data) + if not topdir: + topdir = os.getcwd() + bb.data.setVar('TOPDIR', topdir, data) if not bb.data.getVar('BBPATH', data): - bb.data.setVar('BBPATH', os.path.join(sys.prefix, 'share', 'bitbake'), data) + from pkg_resources import Requirement, resource_filename + bitbake = Requirement.parse("bitbake") + datadir = resource_filename(bitbake, "../share/bitbake") + basedir = resource_filename(bitbake, "..") + bb.data.setVar('BBPATH', '%s:%s:%s' % (topdir, datadir, basedir), data) + def supports(fn, d): return localpath(fn, d)[-5:] == ".conf" diff --git a/bitbake/lib/bb/providers.py b/bitbake/lib/bb/providers.py index 001281a293..8617251ca3 100644 --- a/bitbake/lib/bb/providers.py +++ b/bitbake/lib/bb/providers.py @@ -21,7 +21,7 @@ # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os, re +import re from bb import data, utils import bb @@ -203,7 +203,7 @@ def _filterProviders(providers, item, cfgData, dataCache): eligible.append(preferred_versions[pn][1]) # Now add latest verisons - for pn in pkg_pn.keys(): + for pn in sortpkg_pn.keys(): if pn in preferred_versions and preferred_versions[pn][1]: continue preferred_versions[pn] = findLatestProvider(pn, cfgData, dataCache, sortpkg_pn[pn][0]) diff --git a/bitbake/lib/bb/runqueue.py b/bitbake/lib/bb/runqueue.py index cce5da4057..c3ad442e47 100644 --- a/bitbake/lib/bb/runqueue.py +++ b/bitbake/lib/bb/runqueue.py @@ -37,20 +37,38 @@ class RunQueueStats: """ Holds statistics on the tasks handled by the associated runQueue """ - def __init__(self): + def __init__(self, total): self.completed = 0 self.skipped = 0 self.failed = 0 + self.active = 0 + self.total = total def taskFailed(self): + self.active = self.active - 1 self.failed = self.failed + 1 def taskCompleted(self, number = 1): + self.active = self.active - number self.completed = self.completed + number def taskSkipped(self, number = 1): + self.active = self.active + number self.skipped = self.skipped + number + def taskActive(self): + self.active = self.active + 1 + +# These values indicate the next step due to be run in the +# runQueue state machine +runQueuePrepare = 2 +runQueueRunInit = 3 +runQueueRunning = 4 +runQueueFailed = 6 +runQueueCleanUp = 7 +runQueueComplete = 8 +runQueueChildProcess = 9 + class RunQueueScheduler: """ Control the order tasks are scheduled in. @@ -142,9 +160,9 @@ class RunQueue: self.cooker = cooker self.dataCache = dataCache self.taskData = taskData + self.cfgData = cfgData self.targets = targets - self.cfgdata = cfgData self.number_tasks = int(bb.data.getVar("BB_NUMBER_THREADS", cfgData, 1) or 1) self.multi_provider_whitelist = (bb.data.getVar("MULTI_PROVIDER_WHITELIST", cfgData, 1) or "").split() self.scheduler = bb.data.getVar("BB_SCHEDULER", cfgData, 1) or "speed" @@ -152,12 +170,13 @@ class RunQueue: self.stampwhitelist = bb.data.getVar("BB_STAMP_WHITELIST", cfgData, 1) or "" def reset_runqueue(self): - self.runq_fnid = [] self.runq_task = [] self.runq_depends = [] self.runq_revdeps = [] + self.state = runQueuePrepare + def get_user_idstring(self, task): fn = self.taskData.fn_index[self.runq_fnid[task]] taskname = self.runq_task[task] @@ -653,6 +672,8 @@ class RunQueue: #self.dump_data(taskData) + self.state = runQueueRunInit + def check_stamps(self): unchecked = {} current = [] @@ -796,39 +817,51 @@ class RunQueue: (if the abort on failure configuration option isn't set) """ - failures = 0 - while 1: - failed_fnids = [] - try: - self.execute_runqueue_internal() - finally: - if self.master_process: - failed_fnids = self.finish_runqueue() - if len(failed_fnids) == 0: - return failures + if self.state is runQueuePrepare: + self.prepare_runqueue() + + if self.state is runQueueRunInit: + bb.msg.note(1, bb.msg.domain.RunQueue, "Executing runqueue") + self.execute_runqueue_initVars() + + if self.state is runQueueRunning: + self.execute_runqueue_internal() + + if self.state is runQueueCleanUp: + self.finish_runqueue() + + if self.state is runQueueFailed: if not self.taskData.tryaltconfigs: - raise bb.runqueue.TaskFailure(failed_fnids) - for fnid in failed_fnids: - #print "Failure: %s %s %s" % (fnid, self.taskData.fn_index[fnid], self.runq_task[fnid]) + raise bb.runqueue.TaskFailure(self.failed_fnids) + for fnid in self.failed_fnids: self.taskData.fail_fnid(fnid) - failures = failures + 1 self.reset_runqueue() - self.prepare_runqueue() + + if self.state is runQueueComplete: + # All done + bb.msg.note(1, bb.msg.domain.RunQueue, "Tasks Summary: Attempted %d tasks of which %d didn't need to be rerun and %d failed." % (self.stats.completed, self.stats.skipped, self.stats.failed)) + return False + + if self.state is runQueueChildProcess: + print "Child process" + return False + + # Loop + return True def execute_runqueue_initVars(self): - self.stats = RunQueueStats() + self.stats = RunQueueStats(len(self.runq_fnid)) - self.active_builds = 0 self.runq_buildable = [] self.runq_running = [] self.runq_complete = [] self.build_pids = {} + self.build_pipes = {} self.failed_fnids = [] - self.master_process = True # Mark initial buildable tasks - for task in range(len(self.runq_fnid)): + for task in range(self.stats.total): self.runq_running.append(0) self.runq_complete.append(0) if len(self.runq_depends[task]) == 0: @@ -836,6 +869,10 @@ class RunQueue: else: self.runq_buildable.append(0) + self.state = runQueueRunning + + event.fire(bb.event.StampUpdate(self.target_pairs, self.dataCache.stamp), self.cfgData) + def task_complete(self, task): """ Mark a task as completed @@ -858,26 +895,32 @@ class RunQueue: taskname = self.runq_task[revdep] bb.msg.debug(1, bb.msg.domain.RunQueue, "Marking task %s (%s, %s) as buildable" % (revdep, fn, taskname)) + def task_fail(self, task, exitcode): + """ + Called when a task has failed + Updates the state engine with the failure + """ + bb.msg.error(bb.msg.domain.RunQueue, "Task %s (%s) failed with %s" % (task, self.get_user_idstring(task), exitcode)) + self.stats.taskFailed() + fnid = self.runq_fnid[task] + self.failed_fnids.append(fnid) + bb.event.fire(runQueueTaskFailed(task, self.stats, self), self.cfgData) + if self.taskData.abort: + self.state = runQueueCleanup + def execute_runqueue_internal(self): """ Run the tasks in a queue prepared by prepare_runqueue """ - bb.msg.note(1, bb.msg.domain.RunQueue, "Executing runqueue") - - self.execute_runqueue_initVars() - - if len(self.runq_fnid) == 0: + if self.stats.total == 0: # nothing to do - return [] - - def sigint_handler(signum, frame): - raise KeyboardInterrupt - - event.fire(bb.event.StampUpdate(self.target_pairs, self.dataCache.stamp, self.cfgdata)) + self.state = runQueueCleanup while True: - task = self.sched.next() + task = None + if self.stats.active < self.number_tasks: + task = self.sched.next() if task is not None: fn = self.taskData.fn_index[self.runq_fnid[task]] @@ -885,107 +928,143 @@ class RunQueue: if self.check_stamp_task(task): bb.msg.debug(2, bb.msg.domain.RunQueue, "Stamp current task %s (%s)" % (task, self.get_user_idstring(task))) self.runq_running[task] = 1 + self.runq_buildable[task] = 1 self.task_complete(task) self.stats.taskCompleted() self.stats.taskSkipped() continue - bb.msg.note(1, bb.msg.domain.RunQueue, "Running task %d of %d (ID: %s, %s)" % (self.stats.completed + self.active_builds + 1, len(self.runq_fnid), task, self.get_user_idstring(task))) sys.stdout.flush() sys.stderr.flush() - try: + try: + pipein, pipeout = os.pipe() pid = os.fork() except OSError, e: bb.msg.fatal(bb.msg.domain.RunQueue, "fork failed: %d (%s)" % (e.errno, e.strerror)) if pid == 0: - # Bypass master process' handling - self.master_process = False - # Stop Ctrl+C being sent to children - # signal.signal(signal.SIGINT, signal.SIG_IGN) + os.close(pipein) + # Save out the PID so that the event can include it the + # events + bb.event.worker_pid = os.getpid() + bb.event.worker_pipe = pipeout + + self.state = runQueueChildProcess # Make the child the process group leader os.setpgid(0, 0) + # No stdin newsi = os.open('/dev/null', os.O_RDWR) os.dup2(newsi, sys.stdin.fileno()) - self.cooker.configuration.cmd = taskname[3:] + + bb.event.fire(runQueueTaskStarted(task, self.stats, self), self.cfgData) + bb.msg.note(1, bb.msg.domain.RunQueue, + "Running task %d of %d (ID: %s, %s)" % (self.stats.completed + self.stats.active + 1, + self.stats.total, + task, + self.get_user_idstring(task))) + bb.data.setVar("__RUNQUEUE_DO_NOT_USE_EXTERNALLY", self, self.cooker.configuration.data) try: - self.cooker.tryBuild(fn) + self.cooker.tryBuild(fn, taskname[3:]) except bb.build.EventException: bb.msg.error(bb.msg.domain.Build, "Build of " + fn + " " + taskname + " failed") - sys.exit(1) + os._exit(1) except: bb.msg.error(bb.msg.domain.Build, "Build of " + fn + " " + taskname + " failed") - raise - sys.exit(0) + os._exit(1) + os._exit(0) + self.build_pids[pid] = task + self.build_pipes[pid] = runQueuePipe(pipein, pipeout, self.cfgData) self.runq_running[task] = 1 - self.active_builds = self.active_builds + 1 - if self.active_builds < self.number_tasks: + self.stats.taskActive() + if self.stats.active < self.number_tasks: continue - if self.active_builds > 0: - result = os.waitpid(-1, 0) - self.active_builds = self.active_builds - 1 + + for pipe in self.build_pipes: + self.build_pipes[pipe].read() + + if self.stats.active > 0: + result = os.waitpid(-1, os.WNOHANG) + if result[0] is 0 and result[1] is 0: + return task = self.build_pids[result[0]] + del self.build_pids[result[0]] + self.build_pipes[result[0]].close() + del self.build_pipes[result[0]] if result[1] != 0: - del self.build_pids[result[0]] - bb.msg.error(bb.msg.domain.RunQueue, "Task %s (%s) failed" % (task, self.get_user_idstring(task))) - self.failed_fnids.append(self.runq_fnid[task]) - self.stats.taskFailed() - if not self.taskData.abort: - continue - break + self.task_fail(task, result[1]) + return self.task_complete(task) self.stats.taskCompleted() - del self.build_pids[result[0]] + bb.event.fire(runQueueTaskCompleted(task, self.stats, self), self.cfgData) continue + + if len(self.failed_fnids) != 0: + self.state = runQueueFailed + return + + # Sanity Checks + for task in range(self.stats.total): + if self.runq_buildable[task] == 0: + bb.msg.error(bb.msg.domain.RunQueue, "Task %s never buildable!" % task) + if self.runq_running[task] == 0: + bb.msg.error(bb.msg.domain.RunQueue, "Task %s never ran!" % task) + if self.runq_complete[task] == 0: + bb.msg.error(bb.msg.domain.RunQueue, "Task %s never completed!" % task) + self.state = runQueueComplete return - def finish_runqueue(self): + def finish_runqueue_now(self): + bb.msg.note(1, bb.msg.domain.RunQueue, "Sending SIGINT to remaining %s tasks" % self.stats.active) + for k, v in self.build_pids.iteritems(): + try: + os.kill(-k, signal.SIGINT) + except: + pass + for pipe in self.build_pipes: + self.build_pipes[pipe].read() + + def finish_runqueue(self, now = False): + self.state = runQueueCleanUp + if now: + self.finish_runqueue_now() try: - while self.active_builds > 0: - bb.msg.note(1, bb.msg.domain.RunQueue, "Waiting for %s active tasks to finish" % self.active_builds) + while self.stats.active > 0: + bb.event.fire(runQueueExitWait(self.stats.active), self.cfgData) + bb.msg.note(1, bb.msg.domain.RunQueue, "Waiting for %s active tasks to finish" % self.stats.active) tasknum = 1 for k, v in self.build_pids.iteritems(): - bb.msg.note(1, bb.msg.domain.RunQueue, "%s: %s (%s)" % (tasknum, self.get_user_idstring(v), k)) - tasknum = tasknum + 1 - result = os.waitpid(-1, 0) + bb.msg.note(1, bb.msg.domain.RunQueue, "%s: %s (%s)" % (tasknum, self.get_user_idstring(v), k)) + tasknum = tasknum + 1 + result = os.waitpid(-1, os.WNOHANG) + if result[0] is 0 and result[1] is 0: + return task = self.build_pids[result[0]] - if result[1] != 0: - bb.msg.error(bb.msg.domain.RunQueue, "Task %s (%s) failed" % (task, self.get_user_idstring(task))) - self.failed_fnids.append(self.runq_fnid[task]) - self.stats.taskFailed() del self.build_pids[result[0]] - self.active_builds = self.active_builds - 1 - bb.msg.note(1, bb.msg.domain.RunQueue, "Tasks Summary: Attempted %d tasks of which %d didn't need to be rerun and %d failed." % (self.stats.completed, self.stats.skipped, self.stats.failed)) - return self.failed_fnids - except KeyboardInterrupt: - bb.msg.note(1, bb.msg.domain.RunQueue, "Sending SIGINT to remaining %s tasks" % self.active_builds) - for k, v in self.build_pids.iteritems(): - try: - os.kill(-k, signal.SIGINT) - except: - pass + self.build_pipes[result[0]].close() + del self.build_pipes[result[0]] + if result[1] != 0: + self.task_fail(task, result[1]) + else: + self.stats.taskCompleted() + bb.event.fire(runQueueTaskCompleted(task, self.stats, self), self.cfgData) + except: + self.finish_runqueue_now() raise - # Sanity Checks - for task in range(len(self.runq_fnid)): - if self.runq_buildable[task] == 0: - bb.msg.error(bb.msg.domain.RunQueue, "Task %s never buildable!" % task) - if self.runq_running[task] == 0: - bb.msg.error(bb.msg.domain.RunQueue, "Task %s never ran!" % task) - if self.runq_complete[task] == 0: - bb.msg.error(bb.msg.domain.RunQueue, "Task %s never completed!" % task) - - bb.msg.note(1, bb.msg.domain.RunQueue, "Tasks Summary: Attempted %d tasks of which %d didn't need to be rerun and %d failed." % (self.stats.completed, self.stats.skipped, self.stats.failed)) + if len(self.failed_fnids) != 0: + self.state = runQueueFailed + return - return self.failed_fnids + self.state = runQueueComplete + return def dump_data(self, taskQueue): """ Dump some debug information on the internal data structures """ bb.msg.debug(3, bb.msg.domain.RunQueue, "run_tasks:") - for task in range(len(self.runq_fnid)): + for task in range(len(self.runq_task)): bb.msg.debug(3, bb.msg.domain.RunQueue, " (%s)%s - %s: %s Deps %s RevDeps %s" % (task, taskQueue.fn_index[self.runq_fnid[task]], self.runq_task[task], @@ -994,7 +1073,7 @@ class RunQueue: self.runq_revdeps[task])) bb.msg.debug(3, bb.msg.domain.RunQueue, "sorted_tasks:") - for task1 in range(len(self.runq_fnid)): + for task1 in range(len(self.runq_task)): if task1 in self.prio_map: task = self.prio_map[task1] bb.msg.debug(3, bb.msg.domain.RunQueue, " (%s)%s - %s: %s Deps %s RevDeps %s" % (task, @@ -1005,6 +1084,58 @@ class RunQueue: self.runq_revdeps[task])) +class TaskFailure(Exception): + """ + Exception raised when a task in a runqueue fails + """ + def __init__(self, x): + self.args = x + + +class runQueueExitWait(bb.event.Event): + """ + Event when waiting for task processes to exit + """ + + def __init__(self, remain): + self.remain = remain + self.message = "Waiting for %s active tasks to finish" % remain + bb.event.Event.__init__(self) + +class runQueueEvent(bb.event.Event): + """ + Base runQueue event class + """ + def __init__(self, task, stats, rq): + self.taskid = task + self.taskstring = rq.get_user_idstring(task) + self.stats = stats + bb.event.Event.__init__(self) + +class runQueueTaskStarted(runQueueEvent): + """ + Event notifing a task was started + """ + def __init__(self, task, stats, rq): + runQueueEvent.__init__(self, task, stats, rq) + self.message = "Running task %s (%d of %d) (%s)" % (task, stats.completed + stats.active + 1, self.stats.total, self.taskstring) + +class runQueueTaskFailed(runQueueEvent): + """ + Event notifing a task failed + """ + def __init__(self, task, stats, rq): + runQueueEvent.__init__(self, task, stats, rq) + self.message = "Task %s failed (%s)" % (task, self.taskstring) + +class runQueueTaskCompleted(runQueueEvent): + """ + Event notifing a task completed + """ + def __init__(self, task, stats, rq): + runQueueEvent.__init__(self, task, stats, rq) + self.message = "Task %s completed (%s)" % (task, self.taskstring) + def check_stamp_fn(fn, taskname, d): rq = bb.data.getVar("__RUNQUEUE_DO_NOT_USE_EXTERNALLY", d) fnid = rq.taskData.getfn_id(fn) @@ -1013,3 +1144,31 @@ def check_stamp_fn(fn, taskname, d): return rq.check_stamp_task(taskid) return None +class runQueuePipe(): + """ + Abstraction for a pipe between a worker thread and the server + """ + def __init__(self, pipein, pipeout, d): + self.fd = pipein + os.close(pipeout) + self.queue = "" + self.d = d + + def read(self): + start = len(self.queue) + self.queue = self.queue + os.read(self.fd, 1024) + end = len(self.queue) + index = self.queue.find("") + while index != -1: + bb.event.fire_from_worker(self.queue[:index+8], self.d) + self.queue = self.queue[index+8:] + index = self.queue.find("") + return (end > start) + + def close(self): + while self.read(): + continue + if len(self.queue) > 0: + print "Warning, worker left partial message" + os.close(self.fd) + diff --git a/bitbake/lib/bb/server/__init__.py b/bitbake/lib/bb/server/__init__.py new file mode 100644 index 0000000000..1a732236e2 --- /dev/null +++ b/bitbake/lib/bb/server/__init__.py @@ -0,0 +1,2 @@ +import xmlrpc +import none diff --git a/bitbake/lib/bb/server/none.py b/bitbake/lib/bb/server/none.py new file mode 100644 index 0000000000..ebda111582 --- /dev/null +++ b/bitbake/lib/bb/server/none.py @@ -0,0 +1,181 @@ +# +# BitBake 'dummy' Passthrough Server +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2008 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" + This module implements an xmlrpc server for BitBake. + + Use this by deriving a class from BitBakeXMLRPCServer and then adding + methods which you want to "export" via XMLRPC. If the methods have the + prefix xmlrpc_, then registering those function will happen automatically, + if not, you need to call register_function. + + Use register_idle_function() to add a function which the xmlrpc server + calls from within server_forever when no requests are pending. Make sure + that those functions are non-blocking or else you will introduce latency + in the server's main loop. +""" + +import time +import bb +from bb.ui import uievent +import xmlrpclib +import pickle + +DEBUG = False + +from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler +import inspect, select + +class BitBakeServerCommands(): + def __init__(self, server, cooker): + self.cooker = cooker + self.server = server + + def runCommand(self, command): + """ + Run a cooker command on the server + """ + #print "Running Command %s" % command + return self.cooker.command.runCommand(command) + + def terminateServer(self): + """ + Trigger the server to quit + """ + self.server.server_exit() + #print "Server (cooker) exitting" + return + + def ping(self): + """ + Dummy method which can be used to check the server is still alive + """ + return True + +eventQueue = [] + +class BBUIEventQueue: + class event: + def __init__(self, parent): + self.parent = parent + @staticmethod + def send(event): + bb.server.none.eventQueue.append(pickle.loads(event)) + @staticmethod + def quit(): + return + + def __init__(self, BBServer): + self.eventQueue = bb.server.none.eventQueue + self.BBServer = BBServer + self.EventHandle = bb.event.register_UIHhandler(self) + + def getEvent(self): + if len(self.eventQueue) == 0: + return None + + return self.eventQueue.pop(0) + + def waitEvent(self, delay): + event = self.getEvent() + if event: + return event + self.BBServer.idle_commands(delay) + return self.getEvent() + + def queue_event(self, event): + self.eventQueue.append(event) + + def system_quit( self ): + bb.event.unregister_UIHhandler(self.EventHandle) + +class BitBakeServer(): + # remove this when you're done with debugging + # allow_reuse_address = True + + def __init__(self, cooker): + self._idlefuns = {} + self.commands = BitBakeServerCommands(self, cooker) + + def register_idle_function(self, function, data): + """Register a function to be called while the server is idle""" + assert callable(function) + self._idlefuns[function] = data + + def idle_commands(self, delay): + #print "Idle queue length %s" % len(self._idlefuns) + #print "Idle timeout, running idle functions" + #if len(self._idlefuns) == 0: + nextsleep = delay + for function, data in self._idlefuns.items(): + try: + retval = function(self, data, False) + #print "Idle function returned %s" % (retval) + if retval is False: + del self._idlefuns[function] + elif retval is True: + nextsleep = None + elif nextsleep is None: + continue + elif retval < nextsleep: + nextsleep = retval + except SystemExit: + raise + except: + import traceback + traceback.print_exc() + pass + if nextsleep is not None: + #print "Sleeping for %s (%s)" % (nextsleep, delay) + time.sleep(nextsleep) + + def server_exit(self): + # Tell idle functions we're exiting + for function, data in self._idlefuns.items(): + try: + retval = function(self, data, True) + except: + pass + +class BitbakeServerInfo(): + def __init__(self, server): + self.server = server + self.commands = server.commands + +class BitBakeServerFork(): + def __init__(self, serverinfo, command, logfile): + serverinfo.forkCommand = command + serverinfo.logfile = logfile + +class BitBakeServerConnection(): + def __init__(self, serverinfo): + self.server = serverinfo.server + self.connection = serverinfo.commands + self.events = bb.server.none.BBUIEventQueue(self.server) + + def terminate(self): + try: + self.events.system_quit() + except: + pass + try: + self.connection.terminateServer() + except: + pass + diff --git a/bitbake/lib/bb/server/xmlrpc.py b/bitbake/lib/bb/server/xmlrpc.py new file mode 100644 index 0000000000..3364918c77 --- /dev/null +++ b/bitbake/lib/bb/server/xmlrpc.py @@ -0,0 +1,187 @@ +# +# BitBake XMLRPC Server +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2008 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" + This module implements an xmlrpc server for BitBake. + + Use this by deriving a class from BitBakeXMLRPCServer and then adding + methods which you want to "export" via XMLRPC. If the methods have the + prefix xmlrpc_, then registering those function will happen automatically, + if not, you need to call register_function. + + Use register_idle_function() to add a function which the xmlrpc server + calls from within server_forever when no requests are pending. Make sure + that those functions are non-blocking or else you will introduce latency + in the server's main loop. +""" + +import bb +import xmlrpclib, sys +from bb import daemonize +from bb.ui import uievent + +DEBUG = False + +from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler +import inspect, select + +if sys.hexversion < 0x020600F0: + print "Sorry, python 2.6 or later is required for bitbake's XMLRPC mode" + sys.exit(1) + +class BitBakeServerCommands(): + def __init__(self, server, cooker): + self.cooker = cooker + self.server = server + + def registerEventHandler(self, host, port): + """ + Register a remote UI Event Handler + """ + s = xmlrpclib.Server("http://%s:%d" % (host, port), allow_none=True) + return bb.event.register_UIHhandler(s) + + def unregisterEventHandler(self, handlerNum): + """ + Unregister a remote UI Event Handler + """ + return bb.event.unregister_UIHhandler(handlerNum) + + def runCommand(self, command): + """ + Run a cooker command on the server + """ + return self.cooker.command.runCommand(command) + + def terminateServer(self): + """ + Trigger the server to quit + """ + self.server.quit = True + print "Server (cooker) exitting" + return + + def ping(self): + """ + Dummy method which can be used to check the server is still alive + """ + return True + +class BitBakeServer(SimpleXMLRPCServer): + # remove this when you're done with debugging + # allow_reuse_address = True + + def __init__(self, cooker, interface = ("localhost", 0)): + """ + Constructor + """ + SimpleXMLRPCServer.__init__(self, interface, + requestHandler=SimpleXMLRPCRequestHandler, + logRequests=False, allow_none=True) + self._idlefuns = {} + self.host, self.port = self.socket.getsockname() + #self.register_introspection_functions() + commands = BitBakeServerCommands(self, cooker) + self.autoregister_all_functions(commands, "") + + def autoregister_all_functions(self, context, prefix): + """ + Convenience method for registering all functions in the scope + of this class that start with a common prefix + """ + methodlist = inspect.getmembers(context, inspect.ismethod) + for name, method in methodlist: + if name.startswith(prefix): + self.register_function(method, name[len(prefix):]) + + def register_idle_function(self, function, data): + """Register a function to be called while the server is idle""" + assert callable(function) + self._idlefuns[function] = data + + def serve_forever(self): + """ + Serve Requests. Overloaded to honor a quit command + """ + self.quit = False + self.timeout = 0 # Run Idle calls for our first callback + while not self.quit: + #print "Idle queue length %s" % len(self._idlefuns) + self.handle_request() + #print "Idle timeout, running idle functions" + nextsleep = None + for function, data in self._idlefuns.items(): + try: + retval = function(self, data, False) + if retval is False: + del self._idlefuns[function] + elif retval is True: + nextsleep = 0 + elif nextsleep is 0: + continue + elif nextsleep is None: + nextsleep = retval + elif retval < nextsleep: + nextsleep = retval + except SystemExit: + raise + except: + import traceback + traceback.print_exc() + pass + if nextsleep is None and len(self._idlefuns) > 0: + nextsleep = 0 + self.timeout = nextsleep + # Tell idle functions we're exiting + for function, data in self._idlefuns.items(): + try: + retval = function(self, data, True) + except: + pass + + self.server_close() + return + +class BitbakeServerInfo(): + def __init__(self, server): + self.host = server.host + self.port = server.port + +class BitBakeServerFork(): + def __init__(self, serverinfo, command, logfile): + daemonize.createDaemon(command, logfile) + +class BitBakeServerConnection(): + def __init__(self, serverinfo): + self.connection = xmlrpclib.Server("http://%s:%s" % (serverinfo.host, serverinfo.port), allow_none=True) + self.events = uievent.BBUIEventQueue(self.connection) + + def terminate(self): + # Don't wait for server indefinitely + import socket + socket.setdefaulttimeout(2) + try: + self.events.system_quit() + except: + pass + try: + self.connection.terminateServer() + except: + pass + diff --git a/bitbake/lib/bb/shell.py b/bitbake/lib/bb/shell.py index b1ad78306d..66e51719a4 100644 --- a/bitbake/lib/bb/shell.py +++ b/bitbake/lib/bb/shell.py @@ -151,9 +151,6 @@ class BitBakeShellCommands: if len( names ) == 0: names = [ globexpr ] print "SHELL: Building %s" % ' '.join( names ) - oldcmd = cooker.configuration.cmd - cooker.configuration.cmd = cmd - td = taskdata.TaskData(cooker.configuration.abort) localdata = data.createCopy(cooker.configuration.data) data.update_data(localdata) @@ -168,7 +165,7 @@ class BitBakeShellCommands: if len(providers) == 0: raise Providers.NoProvider - tasks.append([name, "do_%s" % cooker.configuration.cmd]) + tasks.append([name, "do_%s" % cmd]) td.add_unresolved(localdata, cooker.status) @@ -189,7 +186,6 @@ class BitBakeShellCommands: print "ERROR: Couldn't build '%s'" % names last_exception = e - cooker.configuration.cmd = oldcmd build.usage = "" @@ -208,6 +204,11 @@ class BitBakeShellCommands: self.build( params, "configure" ) configure.usage = "" + def install( self, params ): + """Execute 'install' on a providee""" + self.build( params, "install" ) + install.usage = "" + def edit( self, params ): """Call $EDITOR on a providee""" name = params[0] @@ -240,18 +241,14 @@ class BitBakeShellCommands: bf = completeFilePath( name ) print "SHELL: Calling '%s' on '%s'" % ( cmd, bf ) - oldcmd = cooker.configuration.cmd - cooker.configuration.cmd = cmd - try: - cooker.buildFile(bf) + cooker.buildFile(bf, cmd) except parse.ParseError: print "ERROR: Unable to open or parse '%s'" % bf except build.EventException, e: print "ERROR: Couldn't build '%s'" % name last_exception = e - cooker.configuration.cmd = oldcmd fileBuild.usage = "" def fileClean( self, params ): @@ -493,7 +490,7 @@ SRC_URI = "" interpreter.interact( "SHELL: Expert Mode - BitBake Python %s\nType 'help' for more information, press CTRL-D to switch back to BBSHELL." % sys.version ) def showdata( self, params ): - """Show the parsed metadata for a given providee""" + """Execute 'showdata' on a providee""" cooker.showEnvironment(None, params) showdata.usage = "" diff --git a/bitbake/lib/bb/taskdata.py b/bitbake/lib/bb/taskdata.py index 976e0ca1f9..4a88e75f6d 100644 --- a/bitbake/lib/bb/taskdata.py +++ b/bitbake/lib/bb/taskdata.py @@ -23,8 +23,20 @@ Task data collection and handling # with this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -from bb import data, event, mkdirhier, utils -import bb, os +import bb + +def re_match_strings(target, strings): + """ + Whether or not the string 'target' matches + any one string of the strings which can be regular expression string + """ + import re + + for name in strings: + if (name==target or + re.search(name,target)!=None): + return True + return False class TaskData: """ @@ -264,7 +276,7 @@ class TaskData: """ unresolved = [] for target in self.build_names_index: - if target in dataCache.ignored_dependencies: + if re_match_strings(target, dataCache.ignored_dependencies): continue if self.build_names_index.index(target) in self.failed_deps: continue @@ -279,7 +291,7 @@ class TaskData: """ unresolved = [] for target in self.run_names_index: - if target in dataCache.ignored_dependencies: + if re_match_strings(target, dataCache.ignored_dependencies): continue if self.run_names_index.index(target) in self.failed_rdeps: continue @@ -359,7 +371,7 @@ class TaskData: added internally during dependency resolution """ - if item in dataCache.ignored_dependencies: + if re_match_strings(item, dataCache.ignored_dependencies): return if not item in dataCache.providers: @@ -367,7 +379,7 @@ class TaskData: bb.msg.note(2, bb.msg.domain.Provider, "Nothing PROVIDES '%s' (but '%s' DEPENDS on or otherwise requires it)" % (item, self.get_dependees_str(item))) else: bb.msg.note(2, bb.msg.domain.Provider, "Nothing PROVIDES '%s'" % (item)) - bb.event.fire(bb.event.NoProvider(item, cfgData)) + bb.event.fire(bb.event.NoProvider(item), cfgData) raise bb.providers.NoProvider(item) if self.have_build_target(item): @@ -380,7 +392,7 @@ class TaskData: if not eligible: bb.msg.note(2, bb.msg.domain.Provider, "No buildable provider PROVIDES '%s' but '%s' DEPENDS on or otherwise requires it. Enable debugging and see earlier logs to find unbuildable providers." % (item, self.get_dependees_str(item))) - bb.event.fire(bb.event.NoProvider(item, cfgData)) + bb.event.fire(bb.event.NoProvider(item), cfgData) raise bb.providers.NoProvider(item) if len(eligible) > 1 and foundUnique == False: @@ -390,7 +402,7 @@ class TaskData: providers_list.append(dataCache.pkg_fn[fn]) bb.msg.note(1, bb.msg.domain.Provider, "multiple providers are available for %s (%s);" % (item, ", ".join(providers_list))) bb.msg.note(1, bb.msg.domain.Provider, "consider defining PREFERRED_PROVIDER_%s" % item) - bb.event.fire(bb.event.MultipleProviders(item, providers_list, cfgData)) + bb.event.fire(bb.event.MultipleProviders(item, providers_list), cfgData) self.consider_msgs_cache.append(item) for fn in eligible: @@ -410,7 +422,7 @@ class TaskData: (takes item names from RDEPENDS/PACKAGES namespace) """ - if item in dataCache.ignored_dependencies: + if re_match_strings(item, dataCache.ignored_dependencies): return if self.have_runtime_target(item): @@ -420,7 +432,7 @@ class TaskData: if not all_p: bb.msg.error(bb.msg.domain.Provider, "'%s' RDEPENDS/RRECOMMENDS or otherwise requires the runtime entity '%s' but it wasn't found in any PACKAGE or RPROVIDES variables" % (self.get_rdependees_str(item), item)) - bb.event.fire(bb.event.NoProvider(item, cfgData, runtime=True)) + bb.event.fire(bb.event.NoProvider(item, runtime=True), cfgData) raise bb.providers.NoRProvider(item) eligible, numberPreferred = bb.providers.filterProvidersRunTime(all_p, item, cfgData, dataCache) @@ -428,7 +440,7 @@ class TaskData: if not eligible: bb.msg.error(bb.msg.domain.Provider, "'%s' RDEPENDS/RRECOMMENDS or otherwise requires the runtime entity '%s' but it wasn't found in any PACKAGE or RPROVIDES variables of any buildable targets.\nEnable debugging and see earlier logs to find unbuildable targets." % (self.get_rdependees_str(item), item)) - bb.event.fire(bb.event.NoProvider(item, cfgData, runtime=True)) + bb.event.fire(bb.event.NoProvider(item, runtime=True), cfgData) raise bb.providers.NoRProvider(item) if len(eligible) > 1 and numberPreferred == 0: @@ -438,7 +450,7 @@ class TaskData: providers_list.append(dataCache.pkg_fn[fn]) bb.msg.note(2, bb.msg.domain.Provider, "multiple providers are available for runtime %s (%s);" % (item, ", ".join(providers_list))) bb.msg.note(2, bb.msg.domain.Provider, "consider defining a PREFERRED_PROVIDER entry to match runtime %s" % item) - bb.event.fire(bb.event.MultipleProviders(item,providers_list, cfgData, runtime=True)) + bb.event.fire(bb.event.MultipleProviders(item,providers_list, runtime=True), cfgData) self.consider_msgs_cache.append(item) if numberPreferred > 1: @@ -448,7 +460,7 @@ class TaskData: providers_list.append(dataCache.pkg_fn[fn]) bb.msg.note(2, bb.msg.domain.Provider, "multiple providers are available for runtime %s (top %s entries preferred) (%s);" % (item, numberPreferred, ", ".join(providers_list))) bb.msg.note(2, bb.msg.domain.Provider, "consider defining only one PREFERRED_PROVIDER entry to match runtime %s" % item) - bb.event.fire(bb.event.MultipleProviders(item,providers_list, cfgData, runtime=True)) + bb.event.fire(bb.event.MultipleProviders(item,providers_list, runtime=True), cfgData) self.consider_msgs_cache.append(item) # run through the list until we find one that we can build diff --git a/bitbake/lib/bb/ui/__init__.py b/bitbake/lib/bb/ui/__init__.py new file mode 100644 index 0000000000..c6a377a8e6 --- /dev/null +++ b/bitbake/lib/bb/ui/__init__.py @@ -0,0 +1,18 @@ +# +# BitBake UI Implementation +# +# Copyright (C) 2006-2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + diff --git a/bitbake/lib/bb/ui/crumbs/__init__.py b/bitbake/lib/bb/ui/crumbs/__init__.py new file mode 100644 index 0000000000..c6a377a8e6 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/__init__.py @@ -0,0 +1,18 @@ +# +# BitBake UI Implementation +# +# Copyright (C) 2006-2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + diff --git a/bitbake/lib/bb/ui/crumbs/buildmanager.py b/bitbake/lib/bb/ui/crumbs/buildmanager.py new file mode 100644 index 0000000000..f89e8eefd4 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/buildmanager.py @@ -0,0 +1,457 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import gtk +import gobject +import threading +import os +import datetime +import time + +class BuildConfiguration: + """ Represents a potential *or* historic *or* concrete build. It + encompasses all the things that we need to tell bitbake to do to make it + build what we want it to build. + + It also stored the metadata URL and the set of possible machines (and the + distros / images / uris for these. Apart from the metdata URL these are + not serialised to file (since they may be transient). In some ways this + functionality might be shifted to the loader class.""" + + def __init__ (self): + self.metadata_url = None + + # Tuple of (distros, image, urls) + self.machine_options = {} + + self.machine = None + self.distro = None + self.image = None + self.urls = [] + self.extra_urls = [] + self.extra_pkgs = [] + + def get_machines_model (self): + model = gtk.ListStore (gobject.TYPE_STRING) + for machine in self.machine_options.keys(): + model.append ([machine]) + + return model + + def get_distro_and_images_models (self, machine): + distro_model = gtk.ListStore (gobject.TYPE_STRING) + + for distro in self.machine_options[machine][0]: + distro_model.append ([distro]) + + image_model = gtk.ListStore (gobject.TYPE_STRING) + + for image in self.machine_options[machine][1]: + image_model.append ([image]) + + return (distro_model, image_model) + + def get_repos (self): + self.urls = self.machine_options[self.machine][2] + return self.urls + + # It might be a lot lot better if we stored these in like, bitbake conf + # file format. + @staticmethod + def load_from_file (filename): + f = open (filename, "r") + + conf = BuildConfiguration() + for line in f.readlines(): + data = line.split (";")[1] + if (line.startswith ("metadata-url;")): + conf.metadata_url = data.strip() + continue + if (line.startswith ("url;")): + conf.urls += [data.strip()] + continue + if (line.startswith ("extra-url;")): + conf.extra_urls += [data.strip()] + continue + if (line.startswith ("machine;")): + conf.machine = data.strip() + continue + if (line.startswith ("distribution;")): + conf.distro = data.strip() + continue + if (line.startswith ("image;")): + conf.image = data.strip() + continue + + f.close () + return conf + + # Serialise to a file. This is part of the build process and we use this + # to be able to repeat a given build (using the same set of parameters) + # but also so that we can include the details of the image / machine / + # distro in the build manager tree view. + def write_to_file (self, filename): + f = open (filename, "w") + + lines = [] + + if (self.metadata_url): + lines += ["metadata-url;%s\n" % (self.metadata_url)] + + for url in self.urls: + lines += ["url;%s\n" % (url)] + + for url in self.extra_urls: + lines += ["extra-url;%s\n" % (url)] + + if (self.machine): + lines += ["machine;%s\n" % (self.machine)] + + if (self.distro): + lines += ["distribution;%s\n" % (self.distro)] + + if (self.image): + lines += ["image;%s\n" % (self.image)] + + f.writelines (lines) + f.close () + +class BuildResult(gobject.GObject): + """ Represents an historic build. Perhaps not successful. But it includes + things such as the files that are in the directory (the output from the + build) as well as a deserialised BuildConfiguration file that is stored in + ".conf" in the directory for the build. + + This is GObject so that it can be included in the TreeStore.""" + + (STATE_COMPLETE, STATE_FAILED, STATE_ONGOING) = \ + (0, 1, 2) + + def __init__ (self, parent, identifier): + gobject.GObject.__init__ (self) + self.date = None + + self.files = [] + self.status = None + self.identifier = identifier + self.path = os.path.join (parent, identifier) + + # Extract the date, since the directory name is of the + # format build-- we can easily + # pull it out. + # TODO: Better to stat a file? + (_ , date, revision) = identifier.split ("-") + print date + + year = int (date[0:4]) + month = int (date[4:6]) + day = int (date[6:8]) + + self.date = datetime.date (year, month, day) + + self.conf = None + + # By default builds are STATE_FAILED unless we find a "complete" file + # in which case they are STATE_COMPLETE + self.state = BuildResult.STATE_FAILED + for file in os.listdir (self.path): + if (file.startswith (".conf")): + conffile = os.path.join (self.path, file) + self.conf = BuildConfiguration.load_from_file (conffile) + elif (file.startswith ("complete")): + self.state = BuildResult.STATE_COMPLETE + else: + self.add_file (file) + + def add_file (self, file): + # Just add the file for now. Don't care about the type. + self.files += [(file, None)] + +class BuildManagerModel (gtk.TreeStore): + """ Model for the BuildManagerTreeView. This derives from gtk.TreeStore + but it abstracts nicely what the columns mean and the setup of the columns + in the model. """ + + (COL_IDENT, COL_DESC, COL_MACHINE, COL_DISTRO, COL_BUILD_RESULT, COL_DATE, COL_STATE) = \ + (0, 1, 2, 3, 4, 5, 6) + + def __init__ (self): + gtk.TreeStore.__init__ (self, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_OBJECT, + gobject.TYPE_INT64, + gobject.TYPE_INT) + +class BuildManager (gobject.GObject): + """ This class manages the historic builds that have been found in the + "results" directory but is also used for starting a new build.""" + + __gsignals__ = { + 'population-finished' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'populate-error' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()) + } + + def update_build_result (self, result, iter): + # Convert the date into something we can sort by. + date = long (time.mktime (result.date.timetuple())) + + # Add a top level entry for the build + + self.model.set (iter, + BuildManagerModel.COL_IDENT, result.identifier, + BuildManagerModel.COL_DESC, result.conf.image, + BuildManagerModel.COL_MACHINE, result.conf.machine, + BuildManagerModel.COL_DISTRO, result.conf.distro, + BuildManagerModel.COL_BUILD_RESULT, result, + BuildManagerModel.COL_DATE, date, + BuildManagerModel.COL_STATE, result.state) + + # And then we use the files in the directory as the children for the + # top level iter. + for file in result.files: + self.model.append (iter, (None, file[0], None, None, None, date, -1)) + + # This function is called as an idle by the BuildManagerPopulaterThread + def add_build_result (self, result): + gtk.gdk.threads_enter() + self.known_builds += [result] + + self.update_build_result (result, self.model.append (None)) + + gtk.gdk.threads_leave() + + def notify_build_finished (self): + # This is a bit of a hack. If we have a running build running then we + # will have a row in the model in STATE_ONGOING. Find it and make it + # as if it was a proper historic build (well, it is completed now....) + + # We need to use the iters here rather than the Python iterator + # interface to the model since we need to pass it into + # update_build_result + + iter = self.model.get_iter_first() + + while (iter): + (ident, state) = self.model.get(iter, + BuildManagerModel.COL_IDENT, + BuildManagerModel.COL_STATE) + + if state == BuildResult.STATE_ONGOING: + result = BuildResult (self.results_directory, ident) + self.update_build_result (result, iter) + iter = self.model.iter_next(iter) + + def notify_build_succeeded (self): + # Write the "complete" file so that when we create the BuildResult + # object we put into the model + + complete_file_path = os.path.join (self.cur_build_directory, "complete") + f = file (complete_file_path, "w") + f.close() + self.notify_build_finished() + + def notify_build_failed (self): + # Without a "complete" file then this will mark the build as failed: + self.notify_build_finished() + + # This function is called as an idle + def emit_population_finished_signal (self): + gtk.gdk.threads_enter() + self.emit ("population-finished") + gtk.gdk.threads_leave() + + class BuildManagerPopulaterThread (threading.Thread): + def __init__ (self, manager, directory): + threading.Thread.__init__ (self) + self.manager = manager + self.directory = directory + + def run (self): + # For each of the "build-<...>" directories .. + + if os.path.exists (self.directory): + for directory in os.listdir (self.directory): + + if not directory.startswith ("build-"): + continue + + build_result = BuildResult (self.directory, directory) + self.manager.add_build_result (build_result) + + gobject.idle_add (BuildManager.emit_population_finished_signal, + self.manager) + + def __init__ (self, server, results_directory): + gobject.GObject.__init__ (self) + + # The builds that we've found from walking the result directory + self.known_builds = [] + + # Save out the bitbake server, we need this for issuing commands to + # the cooker: + self.server = server + + # The TreeStore that we use + self.model = BuildManagerModel () + + # The results directory is where we create (and look for) the + # build-- directories. We need to populate ourselves from + # directory + self.results_directory = results_directory + self.populate_from_directory (self.results_directory) + + def populate_from_directory (self, directory): + thread = BuildManager.BuildManagerPopulaterThread (self, directory) + thread.start() + + # Come up with the name for the next build ident by combining "build-" + # with the date formatted as yyyymmdd and then an ordinal. We do this by + # an optimistic algorithm incrementing the ordinal if we find that it + # already exists. + def get_next_build_ident (self): + today = datetime.date.today () + datestr = str (today.year) + str (today.month) + str (today.day) + + revision = 0 + test_name = "build-%s-%d" % (datestr, revision) + test_path = os.path.join (self.results_directory, test_name) + + while (os.path.exists (test_path)): + revision += 1 + test_name = "build-%s-%d" % (datestr, revision) + test_path = os.path.join (self.results_directory, test_name) + + return test_name + + # Take a BuildConfiguration and then try and build it based on the + # parameters of that configuration. S + def do_build (self, conf): + server = self.server + + # Work out the build directory. Note we actually create the + # directories here since we need to write the ".conf" file. Otherwise + # we could have relied on bitbake's builder thread to actually make + # the directories as it proceeds with the build. + ident = self.get_next_build_ident () + build_directory = os.path.join (self.results_directory, + ident) + self.cur_build_directory = build_directory + os.makedirs (build_directory) + + conffile = os.path.join (build_directory, ".conf") + conf.write_to_file (conffile) + + # Add a row to the model representing this ongoing build. It's kinda a + # fake entry. If this build completes or fails then this gets updated + # with the real stuff like the historic builds + date = long (time.time()) + self.model.append (None, (ident, conf.image, conf.machine, conf.distro, + None, date, BuildResult.STATE_ONGOING)) + try: + server.runCommand(["setVariable", "BUILD_IMAGES_FROM_FEEDS", 1]) + server.runCommand(["setVariable", "MACHINE", conf.machine]) + server.runCommand(["setVariable", "DISTRO", conf.distro]) + server.runCommand(["setVariable", "PACKAGE_CLASSES", "package_ipk"]) + server.runCommand(["setVariable", "BBFILES", \ + """${OEROOT}/meta/packages/*/*.bb ${OEROOT}/meta-moblin/packages/*/*.bb"""]) + server.runCommand(["setVariable", "TMPDIR", "${OEROOT}/build/tmp"]) + server.runCommand(["setVariable", "IPK_FEED_URIS", \ + " ".join(conf.get_repos())]) + server.runCommand(["setVariable", "DEPLOY_DIR_IMAGE", + build_directory]) + server.runCommand(["buildTargets", [conf.image], "rootfs"]) + + except Exception, e: + print e + +class BuildManagerTreeView (gtk.TreeView): + """ The tree view for the build manager. This shows the historic builds + and so forth. """ + + # We use this function to control what goes in the cell since we store + # the date in the model as seconds since the epoch (for sorting) and so we + # need to make it human readable. + def date_format_custom_cell_data_func (self, col, cell, model, iter): + date = model.get (iter, BuildManagerModel.COL_DATE)[0] + datestr = time.strftime("%A %d %B %Y", time.localtime(date)) + cell.set_property ("text", datestr) + + # This format function controls what goes in the cell. We use this to map + # the integer state to a string and also to colourise the text + def state_format_custom_cell_data_fun (self, col, cell, model, iter): + state = model.get (iter, BuildManagerModel.COL_STATE)[0] + + if (state == BuildResult.STATE_ONGOING): + cell.set_property ("text", "Active") + cell.set_property ("foreground", "#000000") + elif (state == BuildResult.STATE_FAILED): + cell.set_property ("text", "Failed") + cell.set_property ("foreground", "#ff0000") + elif (state == BuildResult.STATE_COMPLETE): + cell.set_property ("text", "Complete") + cell.set_property ("foreground", "#00ff00") + else: + cell.set_property ("text", "") + + def __init__ (self): + gtk.TreeView.__init__(self) + + # Misc descriptiony thing + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn (None, renderer, + text=BuildManagerModel.COL_DESC) + self.append_column (col) + + # Machine + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn ("Machine", renderer, + text=BuildManagerModel.COL_MACHINE) + self.append_column (col) + + # distro + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn ("Distribution", renderer, + text=BuildManagerModel.COL_DISTRO) + self.append_column (col) + + # date (using a custom function for formatting the cell contents it + # takes epoch -> human readable string) + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn ("Date", renderer, + text=BuildManagerModel.COL_DATE) + self.append_column (col) + col.set_cell_data_func (renderer, + self.date_format_custom_cell_data_func) + + # For status. + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn ("Status", renderer, + text = BuildManagerModel.COL_STATE) + self.append_column (col) + col.set_cell_data_func (renderer, + self.state_format_custom_cell_data_fun) + diff --git a/bitbake/lib/bb/ui/crumbs/puccho.glade b/bitbake/lib/bb/ui/crumbs/puccho.glade new file mode 100644 index 0000000000..d7553a6e14 --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/puccho.glade @@ -0,0 +1,606 @@ + + + + + + Start a build + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + 2 + + + True + 6 + 7 + 3 + 5 + 6 + + + True + 12 + + + 6 + + + True + True + 0 + gtk-dialog-error + + + False + False + + + + + True + 0 + If you see this text something is wrong... + True + True + + + 1 + + + + + + + 3 + 2 + 3 + + + + + True + 0 + <b>Build configuration</b> + True + + + 3 + 3 + 4 + + + + + + True + False + + + 1 + 2 + 6 + 7 + + + + + + True + False + 0 + 12 + Image: + + + 6 + 7 + + + + + + True + False + + + 1 + 2 + 5 + 6 + + + + + + True + False + 0 + 12 + Distribution: + + + 5 + 6 + + + + + + True + False + + + 1 + 2 + 4 + 5 + + + + + + True + False + 0 + 12 + Machine: + + + 4 + 5 + + + + + + True + False + True + True + gtk-refresh + True + 0 + + + 2 + 3 + 1 + 2 + + + + + + True + True + 32 + + + 1 + 2 + 1 + 2 + + + + + + True + 0 + 12 + Location: + + + 1 + 2 + + + + + + True + 0 + <b>Repository</b> + True + + + 3 + + + + + + True + + + + + + 2 + 3 + 4 + 5 + + + + + + True + + + + + + 2 + 3 + 5 + 6 + + + + + + True + + + + + + 2 + 3 + 6 + 7 + + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + + + + + + + + + + False + GTK_PACK_END + + + + + + + GTK_WIN_POS_CENTER_ON_PARENT + GDK_WINDOW_TYPE_HINT_DIALOG + False + + + True + 2 + + + True + 6 + 7 + 3 + 6 + 6 + + + True + 0 + <b>Repositories</b> + True + + + 3 + + + + + + True + 0 + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + True + + + + + + + 3 + 2 + 3 + + + + + + True + True + + + 1 + 3 + 1 + 2 + + + + + + True + 0 + <b>Additional packages</b> + True + + + 3 + 4 + 5 + + + + + + True + 0 + 0 + + + True + 0 + 0 + 12 + Location: + + + + + 1 + 2 + + + + + + True + 1 + 0 + + + True + 5 + + + True + True + True + gtk-remove + True + 0 + + + + + True + True + True + gtk-edit + True + 0 + + + 1 + + + + + True + True + True + gtk-add + True + 0 + + + 2 + + + + + + + 1 + 3 + 3 + 4 + + + + + + True + + + + + + 3 + 4 + + + + + + True + 0 + 0 + 12 + Search: + + + 5 + 6 + + + + + + True + True + + + 1 + 3 + 5 + 6 + + + + + + True + 0 + 12 + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + True + True + True + + + + + + + 3 + 6 + 7 + + + + + + 1 + + + + + True + GTK_BUTTONBOX_END + + + True + True + True + gtk-close + True + 0 + + + + + False + GTK_PACK_END + + + + + + + + + True + + + True + + + True + Build + gtk-execute + + + False + + + + + False + + + + + True + True + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + + + + False + True + + + + + True + True + GTK_POLICY_AUTOMATIC + GTK_POLICY_AUTOMATIC + + + + + + True + True + + + + + 1 + + + + + + diff --git a/bitbake/lib/bb/ui/crumbs/runningbuild.py b/bitbake/lib/bb/ui/crumbs/runningbuild.py new file mode 100644 index 0000000000..401559255b --- /dev/null +++ b/bitbake/lib/bb/ui/crumbs/runningbuild.py @@ -0,0 +1,180 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import gtk +import gobject + +class RunningBuildModel (gtk.TreeStore): + (COL_TYPE, COL_PACKAGE, COL_TASK, COL_MESSAGE, COL_ICON, COL_ACTIVE) = (0, 1, 2, 3, 4, 5) + def __init__ (self): + gtk.TreeStore.__init__ (self, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_STRING, + gobject.TYPE_BOOLEAN) + +class RunningBuild (gobject.GObject): + __gsignals__ = { + 'build-succeeded' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'build-failed' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()) + } + pids_to_task = {} + tasks_to_iter = {} + + def __init__ (self): + gobject.GObject.__init__ (self) + self.model = RunningBuildModel() + + def handle_event (self, event): + # Handle an event from the event queue, this may result in updating + # the model and thus the UI. Or it may be to tell us that the build + # has finished successfully (or not, as the case may be.) + + parent = None + pid = 0 + package = None + task = None + + # If we have a pid attached to this message/event try and get the + # (package, task) pair for it. If we get that then get the parent iter + # for the message. + if hassattr(event, 'pid'): + pid = event.pid + if self.pids_to_task.has_key(pid): + (package, task) = self.pids_to_task[pid] + parent = self.tasks_to_iter[(package, task)] + + if isinstance(event, bb.msg.Msg): + # Set a pretty icon for the message based on it's type. + if isinstance(event, bb.msg.MsgWarn): + icon = "dialog-warning" + elif isinstance(event, bb.msg.MsgErr): + icon = "dialog-error" + else: + icon = None + + # Ignore the "Running task i of n .." messages + if (event._message.startswith ("Running task")): + return + + # Add the message to the tree either at the top level if parent is + # None otherwise as a descendent of a task. + self.model.append (parent, + (event.__name__.split()[-1], # e.g. MsgWarn, MsgError + package, + task, + event._message, + icon, + False)) + elif isinstance(event, bb.build.TaskStarted): + (package, task) = (event._package, event._task) + + # Save out this PID. + self.pids_to_task[pid] = (package,task) + + # Check if we already have this package in our model. If so then + # that can be the parent for the task. Otherwise we create a new + # top level for the package. + if (self.tasks_to_iter.has_key ((package, None))): + parent = self.tasks_to_iter[(package, None)] + else: + parent = self.model.append (None, (None, + package, + None, + "Package: %s" % (package), + None, + False)) + self.tasks_to_iter[(package, None)] = parent + + # Because this parent package now has an active child mark it as + # such. + self.model.set(parent, self.model.COL_ICON, "gtk-execute") + + # Add an entry in the model for this task + i = self.model.append (parent, (None, + package, + task, + "Task: %s" % (task), + None, + False)) + + # Save out the iter so that we can find it when we have a message + # that we need to attach to a task. + self.tasks_to_iter[(package, task)] = i + + # Mark this task as active. + self.model.set(i, self.model.COL_ICON, "gtk-execute") + + elif isinstance(event, bb.build.Task): + + if isinstance(event, bb.build.TaskFailed): + # Mark the task as failed + i = self.tasks_to_iter[(package, task)] + self.model.set(i, self.model.COL_ICON, "dialog-error") + + # Mark the parent package as failed + i = self.tasks_to_iter[(package, None)] + self.model.set(i, self.model.COL_ICON, "dialog-error") + else: + # Mark the task as inactive + i = self.tasks_to_iter[(package, task)] + self.model.set(i, self.model.COL_ICON, None) + + # Mark the parent package as inactive + i = self.tasks_to_iter[(package, None)] + self.model.set(i, self.model.COL_ICON, None) + + + # Clear the iters and the pids since when the task goes away the + # pid will no longer be used for messages + del self.tasks_to_iter[(package, task)] + del self.pids_to_task[pid] + + elif isinstance(event, bb.event.BuildCompleted): + failures = int (event._failures) + + # Emit the appropriate signal depending on the number of failures + if (failures > 1): + self.emit ("build-failed") + else: + self.emit ("build-succeeded") + +class RunningBuildTreeView (gtk.TreeView): + def __init__ (self): + gtk.TreeView.__init__ (self) + + # The icon that indicates whether we're building or failed. + renderer = gtk.CellRendererPixbuf () + col = gtk.TreeViewColumn ("Status", renderer) + col.add_attribute (renderer, "icon-name", 4) + self.append_column (col) + + # The message of the build. + renderer = gtk.CellRendererText () + col = gtk.TreeViewColumn ("Message", renderer, text=3) + self.append_column (col) + + diff --git a/bitbake/lib/bb/ui/depexp.py b/bitbake/lib/bb/ui/depexp.py new file mode 100644 index 0000000000..cfa5b6564e --- /dev/null +++ b/bitbake/lib/bb/ui/depexp.py @@ -0,0 +1,272 @@ +# +# BitBake Graphical GTK based Dependency Explorer +# +# Copyright (C) 2007 Ross Burton +# Copyright (C) 2007 - 2008 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import gobject +import gtk +import threading +import xmlrpclib + +# Package Model +(COL_PKG_NAME) = (0) + +# Dependency Model +(TYPE_DEP, TYPE_RDEP) = (0, 1) +(COL_DEP_TYPE, COL_DEP_PARENT, COL_DEP_PACKAGE) = (0, 1, 2) + +class PackageDepView(gtk.TreeView): + def __init__(self, model, dep_type, label): + gtk.TreeView.__init__(self) + self.current = None + self.dep_type = dep_type + self.filter_model = model.filter_new() + self.filter_model.set_visible_func(self._filter) + self.set_model(self.filter_model) + #self.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PACKAGE)) + + def _filter(self, model, iter): + (this_type, package) = model.get(iter, COL_DEP_TYPE, COL_DEP_PARENT) + if this_type != self.dep_type: return False + return package == self.current + + def set_current_package(self, package): + self.current = package + self.filter_model.refilter() + +class PackageReverseDepView(gtk.TreeView): + def __init__(self, model, label): + gtk.TreeView.__init__(self) + self.current = None + self.filter_model = model.filter_new() + self.filter_model.set_visible_func(self._filter) + self.set_model(self.filter_model) + self.append_column(gtk.TreeViewColumn(label, gtk.CellRendererText(), text=COL_DEP_PARENT)) + + def _filter(self, model, iter): + package = model.get_value(iter, COL_DEP_PACKAGE) + return package == self.current + + def set_current_package(self, package): + self.current = package + self.filter_model.refilter() + +class DepExplorer(gtk.Window): + def __init__(self): + gtk.Window.__init__(self) + self.set_title("Dependency Explorer") + self.set_default_size(500, 500) + self.connect("delete-event", gtk.main_quit) + + # Create the data models + self.pkg_model = gtk.ListStore(gobject.TYPE_STRING) + self.depends_model = gtk.ListStore(gobject.TYPE_INT, gobject.TYPE_STRING, gobject.TYPE_STRING) + + pane = gtk.HPaned() + pane.set_position(250) + self.add(pane) + + # The master list of packages + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.pkg_treeview = gtk.TreeView(self.pkg_model) + self.pkg_treeview.get_selection().connect("changed", self.on_cursor_changed) + self.pkg_treeview.append_column(gtk.TreeViewColumn("Package", gtk.CellRendererText(), text=COL_PKG_NAME)) + pane.add1(scrolled) + scrolled.add(self.pkg_treeview) + + box = gtk.VBox(homogeneous=True, spacing=4) + + # Runtime Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.rdep_treeview = PackageDepView(self.depends_model, TYPE_RDEP, "Runtime Depends") + self.rdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + scrolled.add(self.rdep_treeview) + box.add(scrolled) + + # Build Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.dep_treeview = PackageDepView(self.depends_model, TYPE_DEP, "Build Depends") + self.dep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PACKAGE) + scrolled.add(self.dep_treeview) + box.add(scrolled) + pane.add2(box) + + # Reverse Depends + scrolled = gtk.ScrolledWindow() + scrolled.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC) + scrolled.set_shadow_type(gtk.SHADOW_IN) + self.revdep_treeview = PackageReverseDepView(self.depends_model, "Reverse Depends") + self.revdep_treeview.connect("row-activated", self.on_package_activated, COL_DEP_PARENT) + scrolled.add(self.revdep_treeview) + box.add(scrolled) + pane.add2(box) + + self.show_all() + + def on_package_activated(self, treeview, path, column, data_col): + model = treeview.get_model() + package = model.get_value(model.get_iter(path), data_col) + + pkg_path = [] + def finder(model, path, iter, needle): + package = model.get_value(iter, COL_PKG_NAME) + if package == needle: + pkg_path.append(path) + return True + else: + return False + self.pkg_model.foreach(finder, package) + if pkg_path: + self.pkg_treeview.get_selection().select_path(pkg_path[0]) + self.pkg_treeview.scroll_to_cell(pkg_path[0]) + + def on_cursor_changed(self, selection): + (model, it) = selection.get_selected() + if iter is None: + current_package = None + else: + current_package = model.get_value(it, COL_PKG_NAME) + self.rdep_treeview.set_current_package(current_package) + self.dep_treeview.set_current_package(current_package) + self.revdep_treeview.set_current_package(current_package) + + +def parse(depgraph, pkg_model, depends_model): + + for package in depgraph["pn"]: + pkg_model.set(pkg_model.append(), COL_PKG_NAME, package) + + for package in depgraph["depends"]: + for depend in depgraph["depends"][package]: + depends_model.set (depends_model.append(), + COL_DEP_TYPE, TYPE_DEP, + COL_DEP_PARENT, package, + COL_DEP_PACKAGE, depend) + + for package in depgraph["rdepends-pn"]: + for rdepend in depgraph["rdepends-pn"][package]: + depends_model.set (depends_model.append(), + COL_DEP_TYPE, TYPE_RDEP, + COL_DEP_PARENT, package, + COL_DEP_PACKAGE, rdepend) + +class ProgressBar(gtk.Window): + def __init__(self): + + gtk.Window.__init__(self) + self.set_title("Parsing .bb files, please wait...") + self.set_default_size(500, 0) + self.connect("delete-event", gtk.main_quit) + + self.progress = gtk.ProgressBar() + self.add(self.progress) + self.show_all() + +class gtkthread(threading.Thread): + quit = threading.Event() + def __init__(self, shutdown): + threading.Thread.__init__(self) + self.setDaemon(True) + self.shutdown = shutdown + + def run(self): + gobject.threads_init() + gtk.gdk.threads_init() + gtk.main() + gtkthread.quit.set() + +def init(server, eventHandler): + + try: + cmdline = server.runCommand(["getCmdLineAction"]) + if not cmdline or cmdline[0] != "generateDotGraph": + print "This UI is only compatible with the -g option" + return + ret = server.runCommand(["generateDepTreeEvent", cmdline[1], cmdline[2]]) + if ret != True: + print "Couldn't run command! %s" % ret + return + except xmlrpclib.Fault, x: + print "XMLRPC Fault getting commandline:\n %s" % x + return + + shutdown = 0 + + gtkgui = gtkthread(shutdown) + gtkgui.start() + + gtk.gdk.threads_enter() + pbar = ProgressBar() + dep = DepExplorer() + gtk.gdk.threads_leave() + + while True: + try: + event = eventHandler.waitEvent(0.25) + if gtkthread.quit.isSet(): + break + + if event is None: + continue + if isinstance(event, bb.event.ParseProgress): + x = event.sofar + y = event.total + if x == y: + print("\nParsing finished. %d cached, %d parsed, %d skipped, %d masked, %d errors." + % ( event.cached, event.parsed, event.skipped, event.masked, event.errors)) + pbar.hide() + gtk.gdk.threads_enter() + pbar.progress.set_fraction(float(x)/float(y)) + pbar.progress.set_text("%d/%d (%2d %%)" % (x, y, x*100/y)) + gtk.gdk.threads_leave() + continue + + if isinstance(event, bb.event.DepTreeGenerated): + gtk.gdk.threads_enter() + parse(event._depgraph, dep.pkg_model, dep.depends_model) + gtk.gdk.threads_leave() + + if isinstance(event, bb.command.CookerCommandCompleted): + continue + if isinstance(event, bb.command.CookerCommandFailed): + print "Command execution failed: %s" % event.error + break + if isinstance(event, bb.cooker.CookerExit): + break + + continue + + except KeyboardInterrupt: + if shutdown == 2: + print "\nThird Keyboard Interrupt, exit.\n" + break + if shutdown == 1: + print "\nSecond Keyboard Interrupt, stopping...\n" + server.runCommand(["stateStop"]) + if shutdown == 0: + print "\nKeyboard Interrupt, closing down...\n" + server.runCommand(["stateShutdown"]) + shutdown = shutdown + 1 + pass + diff --git a/bitbake/lib/bb/ui/goggle.py b/bitbake/lib/bb/ui/goggle.py new file mode 100644 index 0000000000..94995d82db --- /dev/null +++ b/bitbake/lib/bb/ui/goggle.py @@ -0,0 +1,77 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import gobject +import gtk +import xmlrpclib +from bb.ui.crumbs.runningbuild import RunningBuildTreeView, RunningBuild + +def event_handle_idle_func (eventHandler, build): + + # Consume as many messages as we can in the time available to us + event = eventHandler.getEvent() + while event: + build.handle_event (event) + event = eventHandler.getEvent() + + return True + +class MainWindow (gtk.Window): + def __init__ (self): + gtk.Window.__init__ (self, gtk.WINDOW_TOPLEVEL) + + # Setup tree view and the scrolled window + scrolled_window = gtk.ScrolledWindow () + self.add (scrolled_window) + self.cur_build_tv = RunningBuildTreeView() + scrolled_window.add (self.cur_build_tv) + +def init (server, eventHandler): + gobject.threads_init() + gtk.gdk.threads_init() + + window = MainWindow () + window.show_all () + + # Create the object for the current build + running_build = RunningBuild () + window.cur_build_tv.set_model (running_build.model) + try: + cmdline = server.runCommand(["getCmdLineAction"]) + print cmdline + if not cmdline: + return 1 + ret = server.runCommand(cmdline) + if ret != True: + print "Couldn't get default commandline! %s" % ret + return 1 + except xmlrpclib.Fault, x: + print "XMLRPC Fault getting commandline:\n %s" % x + return 1 + + # Use a timeout function for probing the event queue to find out if we + # have a message waiting for us. + gobject.timeout_add (200, + event_handle_idle_func, + eventHandler, + running_build) + + gtk.main() + diff --git a/bitbake/lib/bb/ui/knotty.py b/bitbake/lib/bb/ui/knotty.py new file mode 100644 index 0000000000..c69fd6ca64 --- /dev/null +++ b/bitbake/lib/bb/ui/knotty.py @@ -0,0 +1,162 @@ +# +# BitBake (No)TTY UI Implementation +# +# Handling output to TTYs or files (no TTY) +# +# Copyright (C) 2006-2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import os + +import sys +import itertools +import xmlrpclib + +parsespin = itertools.cycle( r'|/-\\' ) + +def init(server, eventHandler): + + # Get values of variables which control our output + includelogs = server.runCommand(["getVariable", "BBINCLUDELOGS"]) + loglines = server.runCommand(["getVariable", "BBINCLUDELOGS_LINES"]) + + try: + cmdline = server.runCommand(["getCmdLineAction"]) + #print cmdline + if not cmdline: + return 1 + ret = server.runCommand(cmdline) + if ret != True: + print "Couldn't get default commandline! %s" % ret + return 1 + except xmlrpclib.Fault, x: + print "XMLRPC Fault getting commandline:\n %s" % x + return 1 + + shutdown = 0 + return_value = 0 + while True: + try: + event = eventHandler.waitEvent(0.25) + if event is None: + continue + #print event + if isinstance(event, bb.msg.MsgPlain): + print event._message + continue + if isinstance(event, bb.msg.MsgDebug): + print 'DEBUG: ' + event._message + continue + if isinstance(event, bb.msg.MsgNote): + print 'NOTE: ' + event._message + continue + if isinstance(event, bb.msg.MsgWarn): + print 'WARNING: ' + event._message + continue + if isinstance(event, bb.msg.MsgError): + return_value = 1 + print 'ERROR: ' + event._message + continue + if isinstance(event, bb.msg.MsgFatal): + return_value = 1 + print 'FATAL: ' + event._message + break + if isinstance(event, bb.build.TaskFailed): + return_value = 1 + logfile = event.logfile + if logfile: + print "ERROR: Logfile of failure stored in %s." % logfile + if 1 or includelogs: + print "Log data follows:" + f = open(logfile, "r") + lines = [] + while True: + l = f.readline() + if l == '': + break + l = l.rstrip() + if loglines: + lines.append(' | %s' % l) + if len(lines) > int(loglines): + lines.pop(0) + else: + print '| %s' % l + f.close() + if lines: + for line in lines: + print line + if isinstance(event, bb.build.TaskBase): + print "NOTE: %s" % event._message + continue + if isinstance(event, bb.event.ParseProgress): + x = event.sofar + y = event.total + if os.isatty(sys.stdout.fileno()): + sys.stdout.write("\rNOTE: Handling BitBake files: %s (%04d/%04d) [%2d %%]" % ( parsespin.next(), x, y, x*100/y ) ) + sys.stdout.flush() + else: + if x == 1: + sys.stdout.write("Parsing .bb files, please wait...") + sys.stdout.flush() + if x == y: + sys.stdout.write("done.") + sys.stdout.flush() + if x == y: + print("\nParsing of %d .bb files complete (%d cached, %d parsed). %d targets, %d skipped, %d masked, %d errors." + % ( event.total, event.cached, event.parsed, event.virtuals, event.skipped, event.masked, event.errors)) + continue + + if isinstance(event, bb.command.CookerCommandCompleted): + break + if isinstance(event, bb.command.CookerCommandSetExitCode): + return_value = event.exitcode + continue + if isinstance(event, bb.command.CookerCommandFailed): + return_value = 1 + print "Command execution failed: %s" % event.error + break + if isinstance(event, bb.cooker.CookerExit): + break + + # ignore + if isinstance(event, bb.event.BuildStarted): + continue + if isinstance(event, bb.event.BuildCompleted): + continue + if isinstance(event, bb.event.MultipleProviders): + continue + if isinstance(event, bb.runqueue.runQueueEvent): + continue + if isinstance(event, bb.event.StampUpdate): + continue + if isinstance(event, bb.event.ConfigParsed): + continue + if isinstance(event, bb.event.RecipeParsed): + continue + print "Unknown Event: %s" % event + + except KeyboardInterrupt: + if shutdown == 2: + print "\nThird Keyboard Interrupt, exit.\n" + break + if shutdown == 1: + print "\nSecond Keyboard Interrupt, stopping...\n" + server.runCommand(["stateStop"]) + if shutdown == 0: + print "\nKeyboard Interrupt, closing down...\n" + server.runCommand(["stateShutdown"]) + shutdown = shutdown + 1 + pass + return return_value diff --git a/bitbake/lib/bb/ui/ncurses.py b/bitbake/lib/bb/ui/ncurses.py new file mode 100644 index 0000000000..14310dc124 --- /dev/null +++ b/bitbake/lib/bb/ui/ncurses.py @@ -0,0 +1,335 @@ +# +# BitBake Curses UI Implementation +# +# Implements an ncurses frontend for the BitBake utility. +# +# Copyright (C) 2006 Michael 'Mickey' Lauer +# Copyright (C) 2006-2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +""" + We have the following windows: + + 1.) Main Window: Shows what we are ultimately building and how far we are. Includes status bar + 2.) Thread Activity Window: Shows one status line for every concurrent bitbake thread. + 3.) Command Line Window: Contains an interactive command line where you can interact w/ Bitbake. + + Basic window layout is like that: + + |---------------------------------------------------------| + |
| | + | | 0: foo do_compile complete| + | Building Gtk+-2.6.10 | 1: bar do_patch complete | + | Status: 60% | ... | + | | ... | + | | ... | + |---------------------------------------------------------| + | | + |>>> which virtual/kernel | + |openzaurus-kernel | + |>>> _ | + |---------------------------------------------------------| + +""" + +import os, sys, curses, itertools, time +import bb +import xmlrpclib +from bb import ui +from bb.ui import uihelper + +parsespin = itertools.cycle( r'|/-\\' ) + +X = 0 +Y = 1 +WIDTH = 2 +HEIGHT = 3 + +MAXSTATUSLENGTH = 32 + +class NCursesUI: + """ + NCurses UI Class + """ + class Window: + """Base Window Class""" + def __init__( self, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): + self.win = curses.newwin( height, width, y, x ) + self.dimensions = ( x, y, width, height ) + """ + if curses.has_colors(): + color = 1 + curses.init_pair( color, fg, bg ) + self.win.bkgdset( ord(' '), curses.color_pair(color) ) + else: + self.win.bkgdset( ord(' '), curses.A_BOLD ) + """ + self.erase() + self.setScrolling() + self.win.noutrefresh() + + def erase( self ): + self.win.erase() + + def setScrolling( self, b = True ): + self.win.scrollok( b ) + self.win.idlok( b ) + + def setBoxed( self ): + self.boxed = True + self.win.box() + self.win.noutrefresh() + + def setText( self, x, y, text, *args ): + self.win.addstr( y, x, text, *args ) + self.win.noutrefresh() + + def appendText( self, text, *args ): + self.win.addstr( text, *args ) + self.win.noutrefresh() + + def drawHline( self, y ): + self.win.hline( y, 0, curses.ACS_HLINE, self.dimensions[WIDTH] ) + self.win.noutrefresh() + + class DecoratedWindow( Window ): + """Base class for windows with a box and a title bar""" + def __init__( self, title, x, y, width, height, fg=curses.COLOR_BLACK, bg=curses.COLOR_WHITE ): + NCursesUI.Window.__init__( self, x+1, y+3, width-2, height-4, fg, bg ) + self.decoration = NCursesUI.Window( x, y, width, height, fg, bg ) + self.decoration.setBoxed() + self.decoration.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) + self.setTitle( title ) + + def setTitle( self, title ): + self.decoration.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) + + #-------------------------------------------------------------------------# +# class TitleWindow( Window ): + #-------------------------------------------------------------------------# +# """Title Window""" +# def __init__( self, x, y, width, height ): +# NCursesUI.Window.__init__( self, x, y, width, height ) +# version = bb.__version__ +# title = "BitBake %s" % version +# credit = "(C) 2003-2007 Team BitBake" +# #self.win.hline( 2, 1, curses.ACS_HLINE, width-2 ) +# self.win.border() +# self.setText( 1, 1, title.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) +# self.setText( 1, 2, credit.center( self.dimensions[WIDTH]-2 ), curses.A_BOLD ) + + #-------------------------------------------------------------------------# + class ThreadActivityWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Thread Activity Window""" + def __init__( self, x, y, width, height ): + NCursesUI.DecoratedWindow.__init__( self, "Thread Activity", x, y, width, height ) + + def setStatus( self, thread, text ): + line = "%02d: %s" % ( thread, text ) + width = self.dimensions[WIDTH] + if ( len(line) > width ): + line = line[:width-3] + "..." + else: + line = line.ljust( width ) + self.setText( 0, thread, line ) + + #-------------------------------------------------------------------------# + class MainWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Main Window""" + def __init__( self, x, y, width, height ): + self.StatusPosition = width - MAXSTATUSLENGTH + NCursesUI.DecoratedWindow.__init__( self, None, x, y, width, height ) + curses.nl() + + def setTitle( self, title ): + title = "BitBake %s" % bb.__version__ + self.decoration.setText( 2, 1, title, curses.A_BOLD ) + self.decoration.setText( self.StatusPosition - 8, 1, "Status:", curses.A_BOLD ) + + def setStatus(self, status): + while len(status) < MAXSTATUSLENGTH: + status = status + " " + self.decoration.setText( self.StatusPosition, 1, status, curses.A_BOLD ) + + + #-------------------------------------------------------------------------# + class ShellOutputWindow( DecoratedWindow ): + #-------------------------------------------------------------------------# + """Interactive Command Line Output""" + def __init__( self, x, y, width, height ): + NCursesUI.DecoratedWindow.__init__( self, "Command Line Window", x, y, width, height ) + + #-------------------------------------------------------------------------# + class ShellInputWindow( Window ): + #-------------------------------------------------------------------------# + """Interactive Command Line Input""" + def __init__( self, x, y, width, height ): + NCursesUI.Window.__init__( self, x, y, width, height ) + +# put that to the top again from curses.textpad import Textbox +# self.textbox = Textbox( self.win ) +# t = threading.Thread() +# t.run = self.textbox.edit +# t.start() + + #-------------------------------------------------------------------------# + def main(self, stdscr, server, eventHandler): + #-------------------------------------------------------------------------# + height, width = stdscr.getmaxyx() + + # for now split it like that: + # MAIN_y + THREAD_y = 2/3 screen at the top + # MAIN_x = 2/3 left, THREAD_y = 1/3 right + # CLI_y = 1/3 of screen at the bottom + # CLI_x = full + + main_left = 0 + main_top = 0 + main_height = ( height / 3 * 2 ) + main_width = ( width / 3 ) * 2 + clo_left = main_left + clo_top = main_top + main_height + clo_height = height - main_height - main_top - 1 + clo_width = width + cli_left = main_left + cli_top = clo_top + clo_height + cli_height = 1 + cli_width = width + thread_left = main_left + main_width + thread_top = main_top + thread_height = main_height + thread_width = width - main_width + + #tw = self.TitleWindow( 0, 0, width, main_top ) + mw = self.MainWindow( main_left, main_top, main_width, main_height ) + taw = self.ThreadActivityWindow( thread_left, thread_top, thread_width, thread_height ) + clo = self.ShellOutputWindow( clo_left, clo_top, clo_width, clo_height ) + cli = self.ShellInputWindow( cli_left, cli_top, cli_width, cli_height ) + cli.setText( 0, 0, "BB>" ) + + mw.setStatus("Idle") + + helper = uihelper.BBUIHelper() + shutdown = 0 + + try: + cmdline = server.runCommand(["getCmdLineAction"]) + if not cmdline: + return + ret = server.runCommand(cmdline) + if ret != True: + print "Couldn't get default commandlind! %s" % ret + return + except xmlrpclib.Fault, x: + print "XMLRPC Fault getting commandline:\n %s" % x + return + + exitflag = False + while not exitflag: + try: + event = eventHandler.waitEvent(0.25) + if not event: + continue + helper.eventHandler(event) + #mw.appendText("%s\n" % event[0]) + if isinstance(event, bb.build.Task): + mw.appendText("NOTE: %s\n" % event._message) + if isinstance(event, bb.msg.MsgDebug): + mw.appendText('DEBUG: ' + event._message + '\n') + if isinstance(event, bb.msg.MsgNote): + mw.appendText('NOTE: ' + event._message + '\n') + if isinstance(event, bb.msg.MsgWarn): + mw.appendText('WARNING: ' + event._message + '\n') + if isinstance(event, bb.msg.MsgError): + mw.appendText('ERROR: ' + event._message + '\n') + if isinstance(event, bb.msg.MsgFatal): + mw.appendText('FATAL: ' + event._message + '\n') + if isinstance(event, bb.event.ParseProgress): + x = event.sofar + y = event.total + if x == y: + mw.setStatus("Idle") + mw.appendText("Parsing finished. %d cached, %d parsed, %d skipped, %d masked." + % ( event.cached, event.parsed, event.skipped, event.masked )) + else: + mw.setStatus("Parsing: %s (%04d/%04d) [%2d %%]" % ( parsespin.next(), x, y, x*100/y ) ) +# if isinstance(event, bb.build.TaskFailed): +# if event.logfile: +# if data.getVar("BBINCLUDELOGS", d): +# bb.msg.error(bb.msg.domain.Build, "log data follows (%s)" % logfile) +# number_of_lines = data.getVar("BBINCLUDELOGS_LINES", d) +# if number_of_lines: +# os.system('tail -n%s %s' % (number_of_lines, logfile)) +# else: +# f = open(logfile, "r") +# while True: +# l = f.readline() +# if l == '': +# break +# l = l.rstrip() +# print '| %s' % l +# f.close() +# else: +# bb.msg.error(bb.msg.domain.Build, "see log in %s" % logfile) + + if isinstance(event, bb.command.CookerCommandCompleted): + exitflag = True + if isinstance(event, bb.command.CookerCommandFailed): + mw.appendText("Command execution failed: %s" % event.error) + time.sleep(2) + exitflag = True + if isinstance(event, bb.cooker.CookerExit): + exitflag = True + + if helper.needUpdate: + activetasks, failedtasks = helper.getTasks() + taw.erase() + taw.setText(0, 0, "") + if activetasks: + taw.appendText("Active Tasks:\n") + for task in activetasks: + taw.appendText(task) + if failedtasks: + taw.appendText("Failed Tasks:\n") + for task in failedtasks: + taw.appendText(task) + + curses.doupdate() + except KeyboardInterrupt: + if shutdown == 2: + mw.appendText("Third Keyboard Interrupt, exit.\n") + exitflag = True + if shutdown == 1: + mw.appendText("Second Keyboard Interrupt, stopping...\n") + server.runCommand(["stateStop"]) + if shutdown == 0: + mw.appendText("Keyboard Interrupt, closing down...\n") + server.runCommand(["stateShutdown"]) + shutdown = shutdown + 1 + pass + +def init(server, eventHandler): + if not os.isatty(sys.stdout.fileno()): + print "FATAL: Unable to run 'ncurses' UI without a TTY." + return + ui = NCursesUI() + try: + curses.wrapper(ui.main, server, eventHandler) + except: + import traceback + traceback.print_exc() + diff --git a/bitbake/lib/bb/ui/puccho.py b/bitbake/lib/bb/ui/puccho.py new file mode 100644 index 0000000000..713aa1f4a6 --- /dev/null +++ b/bitbake/lib/bb/ui/puccho.py @@ -0,0 +1,425 @@ +# +# BitBake Graphical GTK User Interface +# +# Copyright (C) 2008 Intel Corporation +# +# Authored by Rob Bradford +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +import gtk +import gobject +import gtk.glade +import threading +import urllib2 +import os + +from bb.ui.crumbs.buildmanager import BuildManager, BuildConfiguration +from bb.ui.crumbs.buildmanager import BuildManagerTreeView + +from bb.ui.crumbs.runningbuild import RunningBuild, RunningBuildTreeView + +# The metadata loader is used by the BuildSetupDialog to download the +# available options to populate the dialog +class MetaDataLoader(gobject.GObject): + """ This class provides the mechanism for loading the metadata (the + fetching and parsing) from a given URL. The metadata encompasses details + on what machines are available. The distribution and images available for + the machine and the the uris to use for building the given machine.""" + __gsignals__ = { + 'success' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + ()), + 'error' : (gobject.SIGNAL_RUN_LAST, + gobject.TYPE_NONE, + (gobject.TYPE_STRING,)) + } + + # We use these little helper functions to ensure that we take the gdk lock + # when emitting the signal. These functions are called as idles (so that + # they happen in the gtk / main thread's main loop. + def emit_error_signal (self, remark): + gtk.gdk.threads_enter() + self.emit ("error", remark) + gtk.gdk.threads_leave() + + def emit_success_signal (self): + gtk.gdk.threads_enter() + self.emit ("success") + gtk.gdk.threads_leave() + + def __init__ (self): + gobject.GObject.__init__ (self) + + class LoaderThread(threading.Thread): + """ This class provides an asynchronous loader for the metadata (by + using threads and signals). This is useful since the metadata may be + at a remote URL.""" + class LoaderImportException (Exception): + pass + + def __init__(self, loader, url): + threading.Thread.__init__ (self) + self.url = url + self.loader = loader + + def run (self): + result = {} + try: + f = urllib2.urlopen (self.url) + + # Parse the metadata format. The format is.... + # ;|...;|...;|... + for line in f.readlines(): + components = line.split(";") + if (len (components) < 4): + raise MetaDataLoader.LoaderThread.LoaderImportException + machine = components[0] + distros = components[1].split("|") + images = components[2].split("|") + urls = components[3].split("|") + + result[machine] = (distros, images, urls) + + # Create an object representing this *potential* + # configuration. It can become concrete if the machine, distro + # and image are all chosen in the UI + configuration = BuildConfiguration() + configuration.metadata_url = self.url + configuration.machine_options = result + self.loader.configuration = configuration + + # Emit that we've actually got a configuration + gobject.idle_add (MetaDataLoader.emit_success_signal, + self.loader) + + except MetaDataLoader.LoaderThread.LoaderImportException, e: + gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader, + "Repository metadata corrupt") + except Exception, e: + gobject.idle_add (MetaDataLoader.emit_error_signal, self.loader, + "Unable to download repository metadata") + print e + + def try_fetch_from_url (self, url): + # Try and download the metadata. Firing a signal if successful + thread = MetaDataLoader.LoaderThread(self, url) + thread.start() + +class BuildSetupDialog (gtk.Dialog): + RESPONSE_BUILD = 1 + + # A little helper method that just sets the states on the widgets based on + # whether we've got good metadata or not. + def set_configurable (self, configurable): + if (self.configurable == configurable): + return + + self.configurable = configurable + for widget in self.conf_widgets: + widget.set_sensitive (configurable) + + if not configurable: + self.machine_combo.set_active (-1) + self.distribution_combo.set_active (-1) + self.image_combo.set_active (-1) + + # GTK widget callbacks + def refresh_button_clicked (self, button): + # Refresh button clicked. + + url = self.location_entry.get_chars (0, -1) + self.loader.try_fetch_from_url(url) + + def repository_entry_editable_changed (self, entry): + if (len (entry.get_chars (0, -1)) > 0): + self.refresh_button.set_sensitive (True) + else: + self.refresh_button.set_sensitive (False) + self.clear_status_message() + + # If we were previously configurable we are no longer since the + # location entry has been changed + self.set_configurable (False) + + def machine_combo_changed (self, combobox): + active_iter = combobox.get_active_iter() + + if not active_iter: + return + + model = combobox.get_model() + + if model: + chosen_machine = model.get (active_iter, 0)[0] + + (distros_model, images_model) = \ + self.loader.configuration.get_distro_and_images_models (chosen_machine) + + self.distribution_combo.set_model (distros_model) + self.image_combo.set_model (images_model) + + # Callbacks from the loader + def loader_success_cb (self, loader): + self.status_image.set_from_icon_name ("info", + gtk.ICON_SIZE_BUTTON) + self.status_image.show() + self.status_label.set_label ("Repository metadata successfully downloaded") + + # Set the models on the combo boxes based on the models generated from + # the configuration that the loader has created + + # We just need to set the machine here, that then determines the + # distro and image options. Cunning huh? :-) + + self.configuration = self.loader.configuration + model = self.configuration.get_machines_model () + self.machine_combo.set_model (model) + + self.set_configurable (True) + + def loader_error_cb (self, loader, message): + self.status_image.set_from_icon_name ("error", + gtk.ICON_SIZE_BUTTON) + self.status_image.show() + self.status_label.set_text ("Error downloading repository metadata") + for widget in self.conf_widgets: + widget.set_sensitive (False) + + def clear_status_message (self): + self.status_image.hide() + self.status_label.set_label ( + """Enter the repository location and press _Refresh""") + + def __init__ (self): + gtk.Dialog.__init__ (self) + + # Cancel + self.add_button (gtk.STOCK_CANCEL, gtk.RESPONSE_CANCEL) + + # Build + button = gtk.Button ("_Build", None, True) + image = gtk.Image () + image.set_from_stock (gtk.STOCK_EXECUTE,gtk.ICON_SIZE_BUTTON) + button.set_image (image) + self.add_action_widget (button, BuildSetupDialog.RESPONSE_BUILD) + button.show_all () + + # Pull in *just* the table from the Glade XML data. + gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade", + root = "build_table") + table = gxml.get_widget ("build_table") + self.vbox.pack_start (table, True, False, 0) + + # Grab all the widgets that we need to turn on/off when we refresh... + self.conf_widgets = [] + self.conf_widgets += [gxml.get_widget ("machine_label")] + self.conf_widgets += [gxml.get_widget ("distribution_label")] + self.conf_widgets += [gxml.get_widget ("image_label")] + self.conf_widgets += [gxml.get_widget ("machine_combo")] + self.conf_widgets += [gxml.get_widget ("distribution_combo")] + self.conf_widgets += [gxml.get_widget ("image_combo")] + + # Grab the status widgets + self.status_image = gxml.get_widget ("status_image") + self.status_label = gxml.get_widget ("status_label") + + # Grab the refresh button and connect to the clicked signal + self.refresh_button = gxml.get_widget ("refresh_button") + self.refresh_button.connect ("clicked", self.refresh_button_clicked) + + # Grab the location entry and connect to editable::changed + self.location_entry = gxml.get_widget ("location_entry") + self.location_entry.connect ("changed", + self.repository_entry_editable_changed) + + # Grab the machine combo and hook onto the changed signal. This then + # allows us to populate the distro and image combos + self.machine_combo = gxml.get_widget ("machine_combo") + self.machine_combo.connect ("changed", self.machine_combo_changed) + + # Setup the combo + cell = gtk.CellRendererText() + self.machine_combo.pack_start(cell, True) + self.machine_combo.add_attribute(cell, 'text', 0) + + # Grab the distro and image combos. We need these to populate with + # models once the machine is chosen + self.distribution_combo = gxml.get_widget ("distribution_combo") + cell = gtk.CellRendererText() + self.distribution_combo.pack_start(cell, True) + self.distribution_combo.add_attribute(cell, 'text', 0) + + self.image_combo = gxml.get_widget ("image_combo") + cell = gtk.CellRendererText() + self.image_combo.pack_start(cell, True) + self.image_combo.add_attribute(cell, 'text', 0) + + # Put the default descriptive text in the status box + self.clear_status_message() + + # Mark as non-configurable, this is just greys out the widgets the + # user can't yet use + self.configurable = False + self.set_configurable(False) + + # Show the table + table.show_all () + + # The loader and some signals connected to it to update the status + # area + self.loader = MetaDataLoader() + self.loader.connect ("success", self.loader_success_cb) + self.loader.connect ("error", self.loader_error_cb) + + def update_configuration (self): + """ A poorly named function but it updates the internal configuration + from the widgets. This can make that configuration concrete and can + thus be used for building """ + # Extract the chosen machine from the combo + model = self.machine_combo.get_model() + active_iter = self.machine_combo.get_active_iter() + if (active_iter): + self.configuration.machine = model.get(active_iter, 0)[0] + + # Extract the chosen distro from the combo + model = self.distribution_combo.get_model() + active_iter = self.distribution_combo.get_active_iter() + if (active_iter): + self.configuration.distro = model.get(active_iter, 0)[0] + + # Extract the chosen image from the combo + model = self.image_combo.get_model() + active_iter = self.image_combo.get_active_iter() + if (active_iter): + self.configuration.image = model.get(active_iter, 0)[0] + +# This function operates to pull events out from the event queue and then push +# them into the RunningBuild (which then drives the RunningBuild which then +# pushes through and updates the progress tree view.) +# +# TODO: Should be a method on the RunningBuild class +def event_handle_timeout (eventHandler, build): + # Consume as many messages as we can ... + event = eventHandler.getEvent() + while event: + build.handle_event (event) + event = eventHandler.getEvent() + return True + +class MainWindow (gtk.Window): + + # Callback that gets fired when the user hits a button in the + # BuildSetupDialog. + def build_dialog_box_response_cb (self, dialog, response_id): + conf = None + if (response_id == BuildSetupDialog.RESPONSE_BUILD): + dialog.update_configuration() + print dialog.configuration.machine, dialog.configuration.distro, \ + dialog.configuration.image + conf = dialog.configuration + + dialog.destroy() + + if conf: + self.manager.do_build (conf) + + def build_button_clicked_cb (self, button): + dialog = BuildSetupDialog () + + # For some unknown reason Dialog.run causes nice little deadlocks ... :-( + dialog.connect ("response", self.build_dialog_box_response_cb) + dialog.show() + + def __init__ (self): + gtk.Window.__init__ (self) + + # Pull in *just* the main vbox from the Glade XML data and then pack + # that inside the window + gxml = gtk.glade.XML (os.path.dirname(__file__) + "/crumbs/puccho.glade", + root = "main_window_vbox") + vbox = gxml.get_widget ("main_window_vbox") + self.add (vbox) + + # Create the tree views for the build manager view and the progress view + self.build_manager_view = BuildManagerTreeView() + self.running_build_view = RunningBuildTreeView() + + # Grab the scrolled windows that we put the tree views into + self.results_scrolledwindow = gxml.get_widget ("results_scrolledwindow") + self.progress_scrolledwindow = gxml.get_widget ("progress_scrolledwindow") + + # Put the tree views inside ... + self.results_scrolledwindow.add (self.build_manager_view) + self.progress_scrolledwindow.add (self.running_build_view) + + # Hook up the build button... + self.build_button = gxml.get_widget ("main_toolbutton_build") + self.build_button.connect ("clicked", self.build_button_clicked_cb) + +# I'm not very happy about the current ownership of the RunningBuild. I have +# my suspicions that this object should be held by the BuildManager since we +# care about the signals in the manager + +def running_build_succeeded_cb (running_build, manager): + # Notify the manager that a build has succeeded. This is necessary as part + # of the 'hack' that we use for making the row in the model / view + # representing the ongoing build change into a row representing the + # completed build. Since we know only one build can be running a time then + # we can handle this. + + # FIXME: Refactor all this so that the RunningBuild is owned by the + # BuildManager. It can then hook onto the signals directly and drive + # interesting things it cares about. + manager.notify_build_succeeded () + print "build succeeded" + +def running_build_failed_cb (running_build, manager): + # As above + print "build failed" + manager.notify_build_failed () + +def init (server, eventHandler): + # Initialise threading... + gobject.threads_init() + gtk.gdk.threads_init() + + main_window = MainWindow () + main_window.show_all () + + # Set up the build manager stuff in general + builds_dir = os.path.join (os.getcwd(), "results") + manager = BuildManager (server, builds_dir) + main_window.build_manager_view.set_model (manager.model) + + # Do the running build setup + running_build = RunningBuild () + main_window.running_build_view.set_model (running_build.model) + running_build.connect ("build-succeeded", running_build_succeeded_cb, + manager) + running_build.connect ("build-failed", running_build_failed_cb, manager) + + # We need to save the manager into the MainWindow so that the toolbar + # button can use it. + # FIXME: Refactor ? + main_window.manager = manager + + # Use a timeout function for probing the event queue to find out if we + # have a message waiting for us. + gobject.timeout_add (200, + event_handle_timeout, + eventHandler, + running_build) + + gtk.main() diff --git a/bitbake/lib/bb/ui/uievent.py b/bitbake/lib/bb/ui/uievent.py new file mode 100644 index 0000000000..36302f4da7 --- /dev/null +++ b/bitbake/lib/bb/ui/uievent.py @@ -0,0 +1,125 @@ +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +""" +Use this class to fork off a thread to recieve event callbacks from the bitbake +server and queue them for the UI to process. This process must be used to avoid +client/server deadlocks. +""" + +import socket, threading, pickle +from SimpleXMLRPCServer import SimpleXMLRPCServer, SimpleXMLRPCRequestHandler + +class BBUIEventQueue: + def __init__(self, BBServer): + + self.eventQueue = [] + self.eventQueueLock = threading.Lock() + self.eventQueueNotify = threading.Event() + + self.BBServer = BBServer + + self.t = threading.Thread() + self.t.setDaemon(True) + self.t.run = self.startCallbackHandler + self.t.start() + + def getEvent(self): + + self.eventQueueLock.acquire() + + if len(self.eventQueue) == 0: + self.eventQueueLock.release() + return None + + item = self.eventQueue.pop(0) + + if len(self.eventQueue) == 0: + self.eventQueueNotify.clear() + + self.eventQueueLock.release() + return item + + def waitEvent(self, delay): + self.eventQueueNotify.wait(delay) + return self.getEvent() + + def queue_event(self, event): + self.eventQueueLock.acquire() + self.eventQueue.append(pickle.loads(event)) + self.eventQueueNotify.set() + self.eventQueueLock.release() + + def startCallbackHandler(self): + + server = UIXMLRPCServer() + self.host, self.port = server.socket.getsockname() + + server.register_function( self.system_quit, "event.quit" ) + server.register_function( self.queue_event, "event.send" ) + server.socket.settimeout(1) + + self.EventHandle = self.BBServer.registerEventHandler(self.host, self.port) + + self.server = server + while not server.quit: + server.handle_request() + server.server_close() + + def system_quit( self ): + """ + Shut down the callback thread + """ + try: + self.BBServer.unregisterEventHandler(self.EventHandle) + except: + pass + self.server.quit = True + +class UIXMLRPCServer (SimpleXMLRPCServer): + + def __init__( self, interface = ("localhost", 0) ): + self.quit = False + SimpleXMLRPCServer.__init__( self, + interface, + requestHandler=SimpleXMLRPCRequestHandler, + logRequests=False, allow_none=True) + + def get_request(self): + while not self.quit: + try: + sock, addr = self.socket.accept() + sock.settimeout(1) + return (sock, addr) + except socket.timeout: + pass + return (None,None) + + def close_request(self, request): + if request is None: + return + SimpleXMLRPCServer.close_request(self, request) + + def process_request(self, request, client_address): + if request is None: + return + SimpleXMLRPCServer.process_request(self, request, client_address) + + diff --git a/bitbake/lib/bb/ui/uihelper.py b/bitbake/lib/bb/ui/uihelper.py new file mode 100644 index 0000000000..151ffc5854 --- /dev/null +++ b/bitbake/lib/bb/ui/uihelper.py @@ -0,0 +1,49 @@ +# ex:ts=4:sw=4:sts=4:et +# -*- tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- +# +# Copyright (C) 2006 - 2007 Michael 'Mickey' Lauer +# Copyright (C) 2006 - 2007 Richard Purdie +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +class BBUIHelper: + def __init__(self): + self.needUpdate = False + self.running_tasks = {} + self.failed_tasks = {} + + def eventHandler(self, event): + if isinstance(event, bb.build.TaskStarted): + self.running_tasks["%s %s\n" % (event._package, event._task)] = "" + self.needUpdate = True + if isinstance(event, bb.build.TaskSucceeded): + del self.running_tasks["%s %s\n" % (event._package, event._task)] + self.needUpdate = True + if isinstance(event, bb.build.TaskFailed): + del self.running_tasks["%s %s\n" % (event._package, event._task)] + self.failed_tasks["%s %s\n" % (event._package, event._task)] = "" + self.needUpdate = True + + # Add runqueue event handling + #if isinstance(event, bb.runqueue.runQueueTaskCompleted): + # a = 1 + #if isinstance(event, bb.runqueue.runQueueTaskStarted): + # a = 1 + #if isinstance(event, bb.runqueue.runQueueTaskFailed): + # a = 1 + #if isinstance(event, bb.runqueue.runQueueExitWait): + # a = 1 + + def getTasks(self): + return (self.running_tasks, self.failed_tasks) diff --git a/bitbake/lib/bb/utils.py b/bitbake/lib/bb/utils.py index 3017ecfa4a..5fc1463e67 100644 --- a/bitbake/lib/bb/utils.py +++ b/bitbake/lib/bb/utils.py @@ -21,8 +21,9 @@ BitBake Utility Functions digits = "0123456789" ascii_letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +separators = ".-" -import re, fcntl, os +import re, fcntl, os, types def explode_version(s): r = [] @@ -39,12 +40,15 @@ def explode_version(s): r.append(m.group(1)) s = m.group(2) continue + r.append(s[0]) s = s[1:] return r def vercmp_part(a, b): va = explode_version(a) vb = explode_version(b) + sa = False + sb = False while True: if va == []: ca = None @@ -56,6 +60,16 @@ def vercmp_part(a, b): cb = vb.pop(0) if ca == None and cb == None: return 0 + + if type(ca) is types.StringType: + sa = ca in separators + if type(cb) is types.StringType: + sb = cb in separators + if sa and not sb: + return -1 + if not sa and sb: + return 1 + if ca > cb: return 1 if ca < cb: @@ -151,7 +165,7 @@ def better_compile(text, file, realfile): # split the text into lines again body = text.split('\n') - bb.msg.error(bb.msg.domain.Util, "Error in compiling: ", realfile) + bb.msg.error(bb.msg.domain.Util, "Error in compiling python function in: ", realfile) bb.msg.error(bb.msg.domain.Util, "The lines resulting into this error were:") bb.msg.error(bb.msg.domain.Util, "\t%d:%s:'%s'" % (e.lineno, e.__class__.__name__, body[e.lineno-1])) @@ -176,7 +190,7 @@ def better_exec(code, context, text, realfile): raise # print the Header of the Error Message - bb.msg.error(bb.msg.domain.Util, "Error in executing: %s" % realfile) + bb.msg.error(bb.msg.domain.Util, "Error in executing python function in: %s" % realfile) bb.msg.error(bb.msg.domain.Util, "Exception:%s Message:%s" % (t,value) ) # let us find the line number now -- cgit v1.2.3-54-g00ecf