summaryrefslogtreecommitdiffstats
path: root/poky/bitbake/lib/bb/tinfoil.py
diff options
context:
space:
mode:
Diffstat (limited to 'poky/bitbake/lib/bb/tinfoil.py')
-rw-r--r--poky/bitbake/lib/bb/tinfoil.py900
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
OpenPOWER on IntegriCloud