diff options
author | Dave Cobbley <david.j.cobbley@linux.intel.com> | 2018-08-14 20:05:37 +0300 |
---|---|---|
committer | Brad Bishop <bradleyb@fuzziesquirrel.com> | 2018-08-23 04:26:31 +0300 |
commit | eb8dc40360f0cfef56fb6947cc817a547d6d9bc6 (patch) | |
tree | de291a73dc37168da6370e2cf16c347d1eba9df8 /poky/bitbake/lib/bb/tinfoil.py | |
parent | 9c3cf826d853102535ead04cebc2d6023eff3032 (diff) | |
download | openbmc-eb8dc40360f0cfef56fb6947cc817a547d6d9bc6.tar.xz |
[Subtree] Removing import-layers directory
As part of the move to subtrees, need to bring all the import layers
content to the top level.
Change-Id: I4a163d10898cbc6e11c27f776f60e1a470049d8f
Signed-off-by: Dave Cobbley <david.j.cobbley@linux.intel.com>
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
Diffstat (limited to 'poky/bitbake/lib/bb/tinfoil.py')
-rw-r--r-- | poky/bitbake/lib/bb/tinfoil.py | 900 |
1 files changed, 900 insertions, 0 deletions
diff --git a/poky/bitbake/lib/bb/tinfoil.py b/poky/bitbake/lib/bb/tinfoil.py new file mode 100644 index 000000000..368264f39 --- /dev/null +++ b/poky/bitbake/lib/bb/tinfoil.py @@ -0,0 +1,900 @@ +# tinfoil: a simple wrapper around cooker for bitbake-based command-line utilities +# +# Copyright (C) 2012-2017 Intel Corporation +# Copyright (C) 2011 Mentor Graphics Corporation +# Copyright (C) 2006-2012 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 logging +import os +import sys +import atexit +import re +from collections import OrderedDict, defaultdict + +import bb.cache +import bb.cooker +import bb.providers +import bb.taskdata +import bb.utils +import bb.command +import bb.remotedata +from bb.cookerdata import CookerConfiguration, ConfigParameters +from bb.main import setup_bitbake, BitBakeConfigParameters, BBMainException +import bb.fetch2 + + +# We need this in order to shut down the connection to the bitbake server, +# otherwise the process will never properly exit +_server_connections = [] +def _terminate_connections(): + for connection in _server_connections: + connection.terminate() +atexit.register(_terminate_connections) + +class TinfoilUIException(Exception): + """Exception raised when the UI returns non-zero from its main function""" + def __init__(self, returncode): + self.returncode = returncode + def __repr__(self): + return 'UI module main returned %d' % self.returncode + +class TinfoilCommandFailed(Exception): + """Exception raised when run_command fails""" + +class TinfoilDataStoreConnector: + """Connector object used to enable access to datastore objects via tinfoil""" + + def __init__(self, tinfoil, dsindex): + self.tinfoil = tinfoil + self.dsindex = dsindex + def getVar(self, name): + value = self.tinfoil.run_command('dataStoreConnectorFindVar', self.dsindex, name) + overrides = None + if isinstance(value, dict): + if '_connector_origtype' in value: + value['_content'] = self.tinfoil._reconvert_type(value['_content'], value['_connector_origtype']) + del value['_connector_origtype'] + if '_connector_overrides' in value: + overrides = value['_connector_overrides'] + del value['_connector_overrides'] + return value, overrides + def getKeys(self): + return set(self.tinfoil.run_command('dataStoreConnectorGetKeys', self.dsindex)) + def getVarHistory(self, name): + return self.tinfoil.run_command('dataStoreConnectorGetVarHistory', self.dsindex, name) + def expandPythonRef(self, varname, expr, d): + ds = bb.remotedata.RemoteDatastores.transmit_datastore(d) + ret = self.tinfoil.run_command('dataStoreConnectorExpandPythonRef', ds, varname, expr) + return ret + def setVar(self, varname, value): + if self.dsindex is None: + self.tinfoil.run_command('setVariable', varname, value) + else: + # Not currently implemented - indicate that setting should + # be redirected to local side + return True + def setVarFlag(self, varname, flagname, value): + if self.dsindex is None: + self.tinfoil.run_command('dataStoreConnectorSetVarFlag', self.dsindex, varname, flagname, value) + else: + # Not currently implemented - indicate that setting should + # be redirected to local side + return True + def delVar(self, varname): + if self.dsindex is None: + self.tinfoil.run_command('dataStoreConnectorDelVar', self.dsindex, varname) + else: + # Not currently implemented - indicate that setting should + # be redirected to local side + return True + def delVarFlag(self, varname, flagname): + if self.dsindex is None: + self.tinfoil.run_command('dataStoreConnectorDelVar', self.dsindex, varname, flagname) + else: + # Not currently implemented - indicate that setting should + # be redirected to local side + return True + def renameVar(self, name, newname): + if self.dsindex is None: + self.tinfoil.run_command('dataStoreConnectorRenameVar', self.dsindex, name, newname) + else: + # Not currently implemented - indicate that setting should + # be redirected to local side + return True + +class TinfoilCookerAdapter: + """ + Provide an adapter for existing code that expects to access a cooker object via Tinfoil, + since now Tinfoil is on the client side it no longer has direct access. + """ + + class TinfoilCookerCollectionAdapter: + """ cooker.collection adapter """ + def __init__(self, tinfoil): + self.tinfoil = tinfoil + def get_file_appends(self, fn): + return self.tinfoil.get_file_appends(fn) + def __getattr__(self, name): + if name == 'overlayed': + return self.tinfoil.get_overlayed_recipes() + elif name == 'bbappends': + return self.tinfoil.run_command('getAllAppends') + else: + raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) + + class TinfoilRecipeCacheAdapter: + """ cooker.recipecache adapter """ + def __init__(self, tinfoil): + self.tinfoil = tinfoil + self._cache = {} + + def get_pkg_pn_fn(self): + pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes') or []) + pkg_fn = {} + for pn, fnlist in pkg_pn.items(): + for fn in fnlist: + pkg_fn[fn] = pn + self._cache['pkg_pn'] = pkg_pn + self._cache['pkg_fn'] = pkg_fn + + def __getattr__(self, name): + # Grab these only when they are requested since they aren't always used + if name in self._cache: + return self._cache[name] + elif name == 'pkg_pn': + self.get_pkg_pn_fn() + return self._cache[name] + elif name == 'pkg_fn': + self.get_pkg_pn_fn() + return self._cache[name] + elif name == 'deps': + attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends') or []) + elif name == 'rundeps': + attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends') or []) + elif name == 'runrecs': + attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends') or []) + elif name == 'pkg_pepvpr': + attrvalue = self.tinfoil.run_command('getRecipeVersions') or {} + elif name == 'inherits': + attrvalue = self.tinfoil.run_command('getRecipeInherits') or {} + elif name == 'bbfile_priority': + attrvalue = self.tinfoil.run_command('getBbFilePriority') or {} + elif name == 'pkg_dp': + attrvalue = self.tinfoil.run_command('getDefaultPreference') or {} + elif name == 'fn_provides': + attrvalue = self.tinfoil.run_command('getRecipeProvides') or {} + elif name == 'packages': + attrvalue = self.tinfoil.run_command('getRecipePackages') or {} + elif name == 'packages_dynamic': + attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic') or {} + elif name == 'rproviders': + attrvalue = self.tinfoil.run_command('getRProviders') or {} + else: + raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) + + self._cache[name] = attrvalue + return attrvalue + + def __init__(self, tinfoil): + self.tinfoil = tinfoil + self.collection = self.TinfoilCookerCollectionAdapter(tinfoil) + self.recipecaches = {} + # FIXME all machines + self.recipecaches[''] = self.TinfoilRecipeCacheAdapter(tinfoil) + self._cache = {} + def __getattr__(self, name): + # Grab these only when they are requested since they aren't always used + if name in self._cache: + return self._cache[name] + elif name == 'skiplist': + attrvalue = self.tinfoil.get_skipped_recipes() + elif name == 'bbfile_config_priorities': + ret = self.tinfoil.run_command('getLayerPriorities') + bbfile_config_priorities = [] + for collection, pattern, regex, pri in ret: + bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri)) + + attrvalue = bbfile_config_priorities + else: + raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) + + self._cache[name] = attrvalue + return attrvalue + + def findBestProvider(self, pn): + return self.tinfoil.find_best_provider(pn) + + +class TinfoilRecipeInfo: + """ + Provides a convenient representation of the cached information for a single recipe. + Some attributes are set on construction, others are read on-demand (which internally + may result in a remote procedure call to the bitbake server the first time). + Note that only information which is cached is available through this object - if + you need other variable values you will need to parse the recipe using + Tinfoil.parse_recipe(). + """ + def __init__(self, recipecache, d, pn, fn, fns): + self._recipecache = recipecache + self._d = d + self.pn = pn + self.fn = fn + self.fns = fns + self.inherit_files = recipecache.inherits[fn] + self.depends = recipecache.deps[fn] + (self.pe, self.pv, self.pr) = recipecache.pkg_pepvpr[fn] + self._cached_packages = None + self._cached_rprovides = None + self._cached_packages_dynamic = None + + def __getattr__(self, name): + if name == 'alternates': + return [x for x in self.fns if x != self.fn] + elif name == 'rdepends': + return self._recipecache.rundeps[self.fn] + elif name == 'rrecommends': + return self._recipecache.runrecs[self.fn] + elif name == 'provides': + return self._recipecache.fn_provides[self.fn] + elif name == 'packages': + if self._cached_packages is None: + self._cached_packages = [] + for pkg, fns in self._recipecache.packages.items(): + if self.fn in fns: + self._cached_packages.append(pkg) + return self._cached_packages + elif name == 'packages_dynamic': + if self._cached_packages_dynamic is None: + self._cached_packages_dynamic = [] + for pkg, fns in self._recipecache.packages_dynamic.items(): + if self.fn in fns: + self._cached_packages_dynamic.append(pkg) + return self._cached_packages_dynamic + elif name == 'rprovides': + if self._cached_rprovides is None: + self._cached_rprovides = [] + for pkg, fns in self._recipecache.rproviders.items(): + if self.fn in fns: + self._cached_rprovides.append(pkg) + return self._cached_rprovides + else: + raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) + def inherits(self, only_recipe=False): + """ + Get the inherited classes for a recipe. Returns the class names only. + Parameters: + only_recipe: True to return only the classes inherited by the recipe + itself, False to return all classes inherited within + the context for the recipe (which includes globally + inherited classes). + """ + if only_recipe: + global_inherit = [x for x in (self._d.getVar('BBINCLUDED') or '').split() if x.endswith('.bbclass')] + else: + global_inherit = [] + for clsfile in self.inherit_files: + if only_recipe and clsfile in global_inherit: + continue + clsname = os.path.splitext(os.path.basename(clsfile))[0] + yield clsname + def __str__(self): + return '%s' % self.pn + + +class Tinfoil: + """ + Tinfoil - an API for scripts and utilities to query + BitBake internals and perform build operations. + """ + + def __init__(self, output=sys.stdout, tracking=False, setup_logging=True): + """ + Create a new tinfoil object. + Parameters: + output: specifies where console output should be sent. Defaults + to sys.stdout. + tracking: True to enable variable history tracking, False to + disable it (default). Enabling this has a minor + performance impact so typically it isn't enabled + unless you need to query variable history. + setup_logging: True to setup a logger so that things like + bb.warn() will work immediately and timeout warnings + are visible; False to let BitBake do this itself. + """ + self.logger = logging.getLogger('BitBake') + self.config_data = None + self.cooker = None + self.tracking = tracking + self.ui_module = None + self.server_connection = None + self.recipes_parsed = False + self.quiet = 0 + self.oldhandlers = self.logger.handlers[:] + if setup_logging: + # This is the *client-side* logger, nothing to do with + # logging messages from the server + bb.msg.logger_create('BitBake', output) + self.localhandlers = [] + for handler in self.logger.handlers: + if handler not in self.oldhandlers: + self.localhandlers.append(handler) + + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.shutdown() + + def prepare(self, config_only=False, config_params=None, quiet=0, extra_features=None): + """ + Prepares the underlying BitBake system to be used via tinfoil. + This function must be called prior to calling any of the other + functions in the API. + NOTE: if you call prepare() you must absolutely call shutdown() + before your code terminates. You can use a "with" block to ensure + this happens e.g. + + with bb.tinfoil.Tinfoil() as tinfoil: + tinfoil.prepare() + ... + + Parameters: + config_only: True to read only the configuration and not load + the cache / parse recipes. This is useful if you just + want to query the value of a variable at the global + level or you want to do anything else that doesn't + involve knowing anything about the recipes in the + current configuration. False loads the cache / parses + recipes. + config_params: optionally specify your own configuration + parameters. If not specified an instance of + TinfoilConfigParameters will be created internally. + quiet: quiet level controlling console output - equivalent + to bitbake's -q/--quiet option. Default of 0 gives + the same output level as normal bitbake execution. + extra_features: extra features to be added to the feature + set requested from the server. See + CookerFeatures._feature_list for possible + features. + """ + self.quiet = quiet + + if self.tracking: + extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING] + else: + extrafeatures = [] + + if extra_features: + extrafeatures += extra_features + + if not config_params: + config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet) + + cookerconfig = CookerConfiguration() + cookerconfig.setConfigParameters(config_params) + + if not config_only: + # Disable local loggers because the UI module is going to set up its own + for handler in self.localhandlers: + self.logger.handlers.remove(handler) + self.localhandlers = [] + + self.server_connection, ui_module = setup_bitbake(config_params, + cookerconfig, + extrafeatures) + + self.ui_module = ui_module + + # Ensure the path to bitbake's bin directory is in PATH so that things like + # bitbake-worker can be run (usually this is the case, but it doesn't have to be) + path = os.getenv('PATH').split(':') + bitbakebinpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'bin')) + for entry in path: + if entry.endswith(os.sep): + entry = entry[:-1] + if os.path.abspath(entry) == bitbakebinpath: + break + else: + path.insert(0, bitbakebinpath) + os.environ['PATH'] = ':'.join(path) + + if self.server_connection: + _server_connections.append(self.server_connection) + if config_only: + config_params.updateToServer(self.server_connection.connection, os.environ.copy()) + self.run_command('parseConfiguration') + else: + self.run_actions(config_params) + self.recipes_parsed = True + + self.config_data = bb.data.init() + connector = TinfoilDataStoreConnector(self, None) + self.config_data.setVar('_remote_data', connector) + self.cooker = TinfoilCookerAdapter(self) + self.cooker_data = self.cooker.recipecaches[''] + else: + raise Exception('Failed to start bitbake server') + + def run_actions(self, config_params): + """ + Run the actions specified in config_params through the UI. + """ + ret = self.ui_module.main(self.server_connection.connection, self.server_connection.events, config_params) + if ret: + raise TinfoilUIException(ret) + + def parseRecipes(self): + """ + Legacy function - use parse_recipes() instead. + """ + self.parse_recipes() + + def parse_recipes(self): + """ + Load information on all recipes. Normally you should specify + config_only=False when calling prepare() instead of using this + function; this function is designed for situations where you need + to initialise Tinfoil and use it with config_only=True first and + then conditionally call this function to parse recipes later. + """ + config_params = TinfoilConfigParameters(config_only=False) + self.run_actions(config_params) + self.recipes_parsed = True + + def run_command(self, command, *params): + """ + Run a command on the server (as implemented in bb.command). + Note that there are two types of command - synchronous and + asynchronous; in order to receive the results of asynchronous + commands you will need to set an appropriate event mask + using set_event_mask() and listen for the result using + wait_event() - with the correct event mask you'll at least get + bb.command.CommandCompleted and possibly other events before + that depending on the command. + """ + if not self.server_connection: + raise Exception('Not connected to server (did you call .prepare()?)') + + commandline = [command] + if params: + commandline.extend(params) + result = self.server_connection.connection.runCommand(commandline) + if result[1]: + raise TinfoilCommandFailed(result[1]) + return result[0] + + def set_event_mask(self, eventlist): + """Set the event mask which will be applied within wait_event()""" + if not self.server_connection: + raise Exception('Not connected to server (did you call .prepare()?)') + llevel, debug_domains = bb.msg.constructLogOptions() + ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist) + if not ret: + raise Exception('setEventMask failed') + + def wait_event(self, timeout=0): + """ + Wait for an event from the server for the specified time. + A timeout of 0 means don't wait if there are no events in the queue. + Returns the next event in the queue or None if the timeout was + reached. Note that in order to recieve any events you will + first need to set the internal event mask using set_event_mask() + (otherwise whatever event mask the UI set up will be in effect). + """ + if not self.server_connection: + raise Exception('Not connected to server (did you call .prepare()?)') + return self.server_connection.events.waitEvent(timeout) + + def get_overlayed_recipes(self): + """ + Find recipes which are overlayed (i.e. where recipes exist in multiple layers) + """ + return defaultdict(list, self.run_command('getOverlayedRecipes')) + + def get_skipped_recipes(self): + """ + Find recipes which were skipped (i.e. SkipRecipe was raised + during parsing). + """ + return OrderedDict(self.run_command('getSkippedRecipes')) + + def get_all_providers(self): + return defaultdict(list, self.run_command('allProviders')) + + def find_providers(self): + return self.run_command('findProviders') + + def find_best_provider(self, pn): + return self.run_command('findBestProvider', pn) + + def get_runtime_providers(self, rdep): + return self.run_command('getRuntimeProviders', rdep) + + def get_recipe_file(self, pn): + """ + Get the file name for the specified recipe/target. Raises + bb.providers.NoProvider if there is no match or the recipe was + skipped. + """ + best = self.find_best_provider(pn) + if not best or (len(best) > 3 and not best[3]): + skiplist = self.get_skipped_recipes() + taskdata = bb.taskdata.TaskData(None, skiplist=skiplist) + skipreasons = taskdata.get_reasons(pn) + if skipreasons: + raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons))) + else: + raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn) + return best[3] + + def get_file_appends(self, fn): + """ + Find the bbappends for a recipe file + """ + return self.run_command('getFileAppends', fn) + + def all_recipes(self, mc='', sort=True): + """ + Enable iterating over all recipes in the current configuration. + Returns an iterator over TinfoilRecipeInfo objects created on demand. + Parameters: + mc: The multiconfig, default of '' uses the main configuration. + sort: True to sort recipes alphabetically (default), False otherwise + """ + recipecache = self.cooker.recipecaches[mc] + if sort: + recipes = sorted(recipecache.pkg_pn.items()) + else: + recipes = recipecache.pkg_pn.items() + for pn, fns in recipes: + prov = self.find_best_provider(pn) + recipe = TinfoilRecipeInfo(recipecache, + self.config_data, + pn=pn, + fn=prov[3], + fns=fns) + yield recipe + + def all_recipe_files(self, mc='', variants=True, preferred_only=False): + """ + Enable iterating over all recipe files in the current configuration. + Returns an iterator over file paths. + Parameters: + mc: The multiconfig, default of '' uses the main configuration. + variants: True to include variants of recipes created through + BBCLASSEXTEND (default) or False to exclude them + preferred_only: True to include only the preferred recipe where + multiple exist providing the same PN, False to list + all recipes + """ + recipecache = self.cooker.recipecaches[mc] + if preferred_only: + files = [] + for pn in recipecache.pkg_pn.keys(): + prov = self.find_best_provider(pn) + files.append(prov[3]) + else: + files = recipecache.pkg_fn.keys() + for fn in sorted(files): + if not variants and fn.startswith('virtual:'): + continue + yield fn + + + def get_recipe_info(self, pn, mc=''): + """ + Get information on a specific recipe in the current configuration by name (PN). + Returns a TinfoilRecipeInfo object created on demand. + Parameters: + mc: The multiconfig, default of '' uses the main configuration. + """ + recipecache = self.cooker.recipecaches[mc] + prov = self.find_best_provider(pn) + fn = prov[3] + if fn: + actual_pn = recipecache.pkg_fn[fn] + recipe = TinfoilRecipeInfo(recipecache, + self.config_data, + pn=actual_pn, + fn=fn, + fns=recipecache.pkg_pn[actual_pn]) + return recipe + else: + return None + + def parse_recipe(self, pn): + """ + Parse the specified recipe and return a datastore object + representing the environment for the recipe. + """ + fn = self.get_recipe_file(pn) + return self.parse_recipe_file(fn) + + def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None): + """ + Parse the specified recipe file (with or without bbappends) + and return a datastore object representing the environment + for the recipe. + Parameters: + fn: recipe file to parse - can be a file path or virtual + specification + appends: True to apply bbappends, False otherwise + appendlist: optional list of bbappend files to apply, if you + want to filter them + config_data: custom config datastore to use. NOTE: if you + specify config_data then you cannot use a virtual + specification for fn. + """ + if self.tracking: + # Enable history tracking just for the parse operation + self.run_command('enableDataTracking') + try: + if appends and appendlist == []: + appends = False + if config_data: + dctr = bb.remotedata.RemoteDatastores.transmit_datastore(config_data) + dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, dctr) + else: + dscon = self.run_command('parseRecipeFile', fn, appends, appendlist) + if dscon: + return self._reconvert_type(dscon, 'DataStoreConnectionHandle') + else: + return None + finally: + if self.tracking: + self.run_command('disableDataTracking') + + def build_file(self, buildfile, task, internal=True): + """ + Runs the specified task for just a single recipe (i.e. no dependencies). + This is equivalent to bitbake -b, except with the default internal=True + no warning about dependencies will be produced, normal info messages + from the runqueue will be silenced and BuildInit, BuildStarted and + BuildCompleted events will not be fired. + """ + return self.run_command('buildFile', buildfile, task, internal) + + def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None): + """ + Builds the specified targets. This is equivalent to a normal invocation + of bitbake. Has built-in event handling which is enabled by default and + can be extended if needed. + Parameters: + targets: + One or more targets to build. Can be a list or a + space-separated string. + task: + The task to run; if None then the value of BB_DEFAULT_TASK + will be used. Default None. + handle_events: + True to handle events in a similar way to normal bitbake + invocation with knotty; False to return immediately (on the + assumption that the caller will handle the events instead). + Default True. + extra_events: + An optional list of events to add to the event mask (if + handle_events=True). If you add events here you also need + to specify a callback function in event_callback that will + handle the additional events. Default None. + event_callback: + An optional function taking a single parameter which + will be called first upon receiving any event (if + handle_events=True) so that the caller can override or + extend the event handling. Default None. + """ + if isinstance(targets, str): + targets = targets.split() + if not task: + task = self.config_data.getVar('BB_DEFAULT_TASK') + + if handle_events: + # A reasonable set of default events matching up with those we handle below + eventmask = [ + 'bb.event.BuildStarted', + 'bb.event.BuildCompleted', + 'logging.LogRecord', + 'bb.event.NoProvider', + 'bb.command.CommandCompleted', + 'bb.command.CommandFailed', + 'bb.build.TaskStarted', + 'bb.build.TaskFailed', + 'bb.build.TaskSucceeded', + 'bb.build.TaskFailedSilent', + 'bb.build.TaskProgress', + 'bb.runqueue.runQueueTaskStarted', + 'bb.runqueue.sceneQueueTaskStarted', + 'bb.event.ProcessStarted', + 'bb.event.ProcessProgress', + 'bb.event.ProcessFinished', + ] + if extra_events: + eventmask.extend(extra_events) + ret = self.set_event_mask(eventmask) + + includelogs = self.config_data.getVar('BBINCLUDELOGS') + loglines = self.config_data.getVar('BBINCLUDELOGS_LINES') + + ret = self.run_command('buildTargets', targets, task) + if handle_events: + result = False + # Borrowed from knotty, instead somewhat hackily we use the helper + # as the object to store "shutdown" on + helper = bb.ui.uihelper.BBUIHelper() + # We set up logging optionally in the constructor so now we need to + # grab the handlers to pass to TerminalFilter + console = None + errconsole = None + for handler in self.logger.handlers: + if isinstance(handler, logging.StreamHandler): + if handler.stream == sys.stdout: + console = handler + elif handler.stream == sys.stderr: + errconsole = handler + format_str = "%(levelname)s: %(message)s" + format = bb.msg.BBLogFormatter(format_str) + helper.shutdown = 0 + parseprogress = None + termfilter = bb.ui.knotty.TerminalFilter(helper, helper, console, errconsole, format, quiet=self.quiet) + try: + while True: + try: + event = self.wait_event(0.25) + if event: + if event_callback and event_callback(event): + continue + if helper.eventHandler(event): + if isinstance(event, bb.build.TaskFailedSilent): + logger.warning("Logfile for failed setscene task is %s" % event.logfile) + elif isinstance(event, bb.build.TaskFailed): + bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter) + continue + if isinstance(event, bb.event.ProcessStarted): + if self.quiet > 1: + continue + parseprogress = bb.ui.knotty.new_progress(event.processname, event.total) + parseprogress.start(False) + continue + if isinstance(event, bb.event.ProcessProgress): + if self.quiet > 1: + continue + if parseprogress: + parseprogress.update(event.progress) + else: + bb.warn("Got ProcessProgress event for someting that never started?") + continue + if isinstance(event, bb.event.ProcessFinished): + if self.quiet > 1: + continue + if parseprogress: + parseprogress.finish() + parseprogress = None + continue + if isinstance(event, bb.command.CommandCompleted): + result = True + break + if isinstance(event, bb.command.CommandFailed): + self.logger.error(str(event)) + result = False + break + if isinstance(event, logging.LogRecord): + if event.taskpid == 0 or event.levelno > logging.INFO: + self.logger.handle(event) + continue + if isinstance(event, bb.event.NoProvider): + self.logger.error(str(event)) + result = False + break + + elif helper.shutdown > 1: + break + termfilter.updateFooter() + except KeyboardInterrupt: + termfilter.clearFooter() + if helper.shutdown == 1: + print("\nSecond Keyboard Interrupt, stopping...\n") + ret = self.run_command("stateForceShutdown") + if ret and ret[2]: + self.logger.error("Unable to cleanly stop: %s" % ret[2]) + elif helper.shutdown == 0: + print("\nKeyboard Interrupt, closing down...\n") + interrupted = True + ret = self.run_command("stateShutdown") + if ret and ret[2]: + self.logger.error("Unable to cleanly shutdown: %s" % ret[2]) + helper.shutdown = helper.shutdown + 1 + termfilter.clearFooter() + finally: + termfilter.finish() + if helper.failed_tasks: + result = False + return result + else: + return ret + + def shutdown(self): + """ + Shut down tinfoil. Disconnects from the server and gracefully + releases any associated resources. You must call this function if + prepare() has been called, or use a with... block when you create + the tinfoil object which will ensure that it gets called. + """ + if self.server_connection: + self.run_command('clientComplete') + _server_connections.remove(self.server_connection) + bb.event.ui_queue = [] + self.server_connection.terminate() + self.server_connection = None + + # Restore logging handlers to how it looked when we started + if self.oldhandlers: + for handler in self.logger.handlers: + if handler not in self.oldhandlers: + self.logger.handlers.remove(handler) + + def _reconvert_type(self, obj, origtypename): + """ + Convert an object back to the right type, in the case + that marshalling has changed it (especially with xmlrpc) + """ + supported_types = { + 'set': set, + 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle, + } + + origtype = supported_types.get(origtypename, None) + if origtype is None: + raise Exception('Unsupported type "%s"' % origtypename) + if type(obj) == origtype: + newobj = obj + elif isinstance(obj, dict): + # New style class + newobj = origtype() + for k,v in obj.items(): + setattr(newobj, k, v) + else: + # Assume we can coerce the type + newobj = origtype(obj) + + if isinstance(newobj, bb.command.DataStoreConnectionHandle): + connector = TinfoilDataStoreConnector(self, newobj.dsindex) + newobj = bb.data.init() + newobj.setVar('_remote_data', connector) + + return newobj + + +class TinfoilConfigParameters(BitBakeConfigParameters): + + def __init__(self, config_only, **options): + self.initial_options = options + # Apply some sane defaults + if not 'parse_only' in options: + self.initial_options['parse_only'] = not config_only + #if not 'status_only' in options: + # self.initial_options['status_only'] = config_only + if not 'ui' in options: + self.initial_options['ui'] = 'knotty' + if not 'argv' in options: + self.initial_options['argv'] = [] + + super(TinfoilConfigParameters, self).__init__() + + def parseCommandLine(self, argv=None): + # We don't want any parameters parsed from the command line + opts = super(TinfoilConfigParameters, self).parseCommandLine([]) + for key, val in self.initial_options.items(): + setattr(opts[0], key, val) + return opts |