xref: /openbmc/openbmc/poky/bitbake/lib/bb/tinfoil.py (revision 220dafdb)
1# tinfoil: a simple wrapper around cooker for bitbake-based command-line utilities
2#
3# Copyright (C) 2012-2017 Intel Corporation
4# Copyright (C) 2011 Mentor Graphics Corporation
5# Copyright (C) 2006-2012 Richard Purdie
6#
7# SPDX-License-Identifier: GPL-2.0-only
8#
9
10import logging
11import os
12import sys
13import time
14import atexit
15import re
16from collections import OrderedDict, defaultdict
17from functools import partial
18
19import bb.cache
20import bb.cooker
21import bb.providers
22import bb.taskdata
23import bb.utils
24import bb.command
25import bb.remotedata
26from bb.main import setup_bitbake, BitBakeConfigParameters
27import bb.fetch2
28
29
30# We need this in order to shut down the connection to the bitbake server,
31# otherwise the process will never properly exit
32_server_connections = []
33def _terminate_connections():
34    for connection in _server_connections:
35        connection.terminate()
36atexit.register(_terminate_connections)
37
38class TinfoilUIException(Exception):
39    """Exception raised when the UI returns non-zero from its main function"""
40    def __init__(self, returncode):
41        self.returncode = returncode
42    def __repr__(self):
43        return 'UI module main returned %d' % self.returncode
44
45class TinfoilCommandFailed(Exception):
46    """Exception raised when run_command fails"""
47
48class TinfoilDataStoreConnectorVarHistory:
49    def __init__(self, tinfoil, dsindex):
50        self.tinfoil = tinfoil
51        self.dsindex = dsindex
52
53    def remoteCommand(self, cmd, *args, **kwargs):
54        return self.tinfoil.run_command('dataStoreConnectorVarHistCmd', self.dsindex, cmd, args, kwargs)
55
56    def emit(self, var, oval, val, o, d):
57        ret = self.tinfoil.run_command('dataStoreConnectorVarHistCmdEmit', self.dsindex, var, oval, val, d.dsindex)
58        o.write(ret)
59
60    def __getattr__(self, name):
61        if not hasattr(bb.data_smart.VariableHistory, name):
62            raise AttributeError("VariableHistory has no such method %s" % name)
63
64        newfunc = partial(self.remoteCommand, name)
65        setattr(self, name, newfunc)
66        return newfunc
67
68class TinfoilDataStoreConnectorIncHistory:
69    def __init__(self, tinfoil, dsindex):
70        self.tinfoil = tinfoil
71        self.dsindex = dsindex
72
73    def remoteCommand(self, cmd, *args, **kwargs):
74        return self.tinfoil.run_command('dataStoreConnectorIncHistCmd', self.dsindex, cmd, args, kwargs)
75
76    def __getattr__(self, name):
77        if not hasattr(bb.data_smart.IncludeHistory, name):
78            raise AttributeError("IncludeHistory has no such method %s" % name)
79
80        newfunc = partial(self.remoteCommand, name)
81        setattr(self, name, newfunc)
82        return newfunc
83
84class TinfoilDataStoreConnector:
85    """
86    Connector object used to enable access to datastore objects via tinfoil
87    Method calls are transmitted to the remote datastore for processing, if a datastore is
88    returned we return a connector object for the new store
89    """
90
91    def __init__(self, tinfoil, dsindex):
92        self.tinfoil = tinfoil
93        self.dsindex = dsindex
94        self.varhistory = TinfoilDataStoreConnectorVarHistory(tinfoil, dsindex)
95        self.inchistory = TinfoilDataStoreConnectorIncHistory(tinfoil, dsindex)
96
97    def remoteCommand(self, cmd, *args, **kwargs):
98        ret = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, cmd, args, kwargs)
99        if isinstance(ret, bb.command.DataStoreConnectionHandle):
100            return TinfoilDataStoreConnector(self.tinfoil, ret.dsindex)
101        return ret
102
103    def __getattr__(self, name):
104        if not hasattr(bb.data._dict_type, name):
105            raise AttributeError("Data store has no such method %s" % name)
106
107        newfunc = partial(self.remoteCommand, name)
108        setattr(self, name, newfunc)
109        return newfunc
110
111    def __iter__(self):
112        keys = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, "keys", [], {})
113        for k in keys:
114            yield k
115
116class TinfoilCookerAdapter:
117    """
118    Provide an adapter for existing code that expects to access a cooker object via Tinfoil,
119    since now Tinfoil is on the client side it no longer has direct access.
120    """
121
122    class TinfoilCookerCollectionAdapter:
123        """ cooker.collection adapter """
124        def __init__(self, tinfoil, mc=''):
125            self.tinfoil = tinfoil
126            self.mc = mc
127        def get_file_appends(self, fn):
128            return self.tinfoil.get_file_appends(fn, self.mc)
129        def __getattr__(self, name):
130            if name == 'overlayed':
131                return self.tinfoil.get_overlayed_recipes(self.mc)
132            elif name == 'bbappends':
133                return self.tinfoil.run_command('getAllAppends', self.mc)
134            else:
135                raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
136
137    class TinfoilRecipeCacheAdapter:
138        """ cooker.recipecache adapter """
139        def __init__(self, tinfoil, mc=''):
140            self.tinfoil = tinfoil
141            self.mc = mc
142            self._cache = {}
143
144        def get_pkg_pn_fn(self):
145            pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes', self.mc) or [])
146            pkg_fn = {}
147            for pn, fnlist in pkg_pn.items():
148                for fn in fnlist:
149                    pkg_fn[fn] = pn
150            self._cache['pkg_pn'] = pkg_pn
151            self._cache['pkg_fn'] = pkg_fn
152
153        def __getattr__(self, name):
154            # Grab these only when they are requested since they aren't always used
155            if name in self._cache:
156                return self._cache[name]
157            elif name == 'pkg_pn':
158                self.get_pkg_pn_fn()
159                return self._cache[name]
160            elif name == 'pkg_fn':
161                self.get_pkg_pn_fn()
162                return self._cache[name]
163            elif name == 'deps':
164                attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends', self.mc) or [])
165            elif name == 'rundeps':
166                attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends', self.mc) or [])
167            elif name == 'runrecs':
168                attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends', self.mc) or [])
169            elif name == 'pkg_pepvpr':
170                attrvalue = self.tinfoil.run_command('getRecipeVersions', self.mc) or {}
171            elif name == 'inherits':
172                attrvalue = self.tinfoil.run_command('getRecipeInherits', self.mc) or {}
173            elif name == 'bbfile_priority':
174                attrvalue = self.tinfoil.run_command('getBbFilePriority', self.mc) or {}
175            elif name == 'pkg_dp':
176                attrvalue = self.tinfoil.run_command('getDefaultPreference', self.mc) or {}
177            elif name == 'fn_provides':
178                attrvalue = self.tinfoil.run_command('getRecipeProvides', self.mc) or {}
179            elif name == 'packages':
180                attrvalue = self.tinfoil.run_command('getRecipePackages', self.mc) or {}
181            elif name == 'packages_dynamic':
182                attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic', self.mc) or {}
183            elif name == 'rproviders':
184                attrvalue = self.tinfoil.run_command('getRProviders', self.mc) or {}
185            else:
186                raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
187
188            self._cache[name] = attrvalue
189            return attrvalue
190
191    def __init__(self, tinfoil):
192        self.tinfoil = tinfoil
193        self.multiconfigs = [''] + (tinfoil.config_data.getVar('BBMULTICONFIG') or '').split()
194        self.collections = {}
195        self.recipecaches = {}
196        for mc in self.multiconfigs:
197            self.collections[mc] = self.TinfoilCookerCollectionAdapter(tinfoil, mc)
198            self.recipecaches[mc] = self.TinfoilRecipeCacheAdapter(tinfoil, mc)
199        self._cache = {}
200    def __getattr__(self, name):
201        # Grab these only when they are requested since they aren't always used
202        if name in self._cache:
203            return self._cache[name]
204        elif name == 'skiplist':
205            attrvalue = self.tinfoil.get_skipped_recipes()
206        elif name == 'bbfile_config_priorities':
207            ret = self.tinfoil.run_command('getLayerPriorities')
208            bbfile_config_priorities = []
209            for collection, pattern, regex, pri in ret:
210                bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri))
211
212            attrvalue = bbfile_config_priorities
213        else:
214            raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
215
216        self._cache[name] = attrvalue
217        return attrvalue
218
219    def findBestProvider(self, pn):
220        return self.tinfoil.find_best_provider(pn)
221
222
223class TinfoilRecipeInfo:
224    """
225    Provides a convenient representation of the cached information for a single recipe.
226    Some attributes are set on construction, others are read on-demand (which internally
227    may result in a remote procedure call to the bitbake server the first time).
228    Note that only information which is cached is available through this object - if
229    you need other variable values you will need to parse the recipe using
230    Tinfoil.parse_recipe().
231    """
232    def __init__(self, recipecache, d, pn, fn, fns):
233        self._recipecache = recipecache
234        self._d = d
235        self.pn = pn
236        self.fn = fn
237        self.fns = fns
238        self.inherit_files = recipecache.inherits[fn]
239        self.depends = recipecache.deps[fn]
240        (self.pe, self.pv, self.pr) = recipecache.pkg_pepvpr[fn]
241        self._cached_packages = None
242        self._cached_rprovides = None
243        self._cached_packages_dynamic = None
244
245    def __getattr__(self, name):
246        if name == 'alternates':
247            return [x for x in self.fns if x != self.fn]
248        elif name == 'rdepends':
249            return self._recipecache.rundeps[self.fn]
250        elif name == 'rrecommends':
251            return self._recipecache.runrecs[self.fn]
252        elif name == 'provides':
253            return self._recipecache.fn_provides[self.fn]
254        elif name == 'packages':
255            if self._cached_packages is None:
256                self._cached_packages = []
257                for pkg, fns in self._recipecache.packages.items():
258                    if self.fn in fns:
259                        self._cached_packages.append(pkg)
260            return self._cached_packages
261        elif name == 'packages_dynamic':
262            if self._cached_packages_dynamic is None:
263                self._cached_packages_dynamic = []
264                for pkg, fns in self._recipecache.packages_dynamic.items():
265                    if self.fn in fns:
266                        self._cached_packages_dynamic.append(pkg)
267            return self._cached_packages_dynamic
268        elif name == 'rprovides':
269            if self._cached_rprovides is None:
270                self._cached_rprovides = []
271                for pkg, fns in self._recipecache.rproviders.items():
272                    if self.fn in fns:
273                        self._cached_rprovides.append(pkg)
274            return self._cached_rprovides
275        else:
276            raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
277    def inherits(self, only_recipe=False):
278        """
279        Get the inherited classes for a recipe. Returns the class names only.
280        Parameters:
281            only_recipe: True to return only the classes inherited by the recipe
282                         itself, False to return all classes inherited within
283                         the context for the recipe (which includes globally
284                         inherited classes).
285        """
286        if only_recipe:
287            global_inherit = [x for x in (self._d.getVar('BBINCLUDED') or '').split() if x.endswith('.bbclass')]
288        else:
289            global_inherit = []
290        for clsfile in self.inherit_files:
291            if only_recipe and clsfile in global_inherit:
292                continue
293            clsname = os.path.splitext(os.path.basename(clsfile))[0]
294            yield clsname
295    def __str__(self):
296        return '%s' % self.pn
297
298
299class Tinfoil:
300    """
301    Tinfoil - an API for scripts and utilities to query
302    BitBake internals and perform build operations.
303    """
304
305    def __init__(self, output=sys.stdout, tracking=False, setup_logging=True):
306        """
307        Create a new tinfoil object.
308        Parameters:
309            output: specifies where console output should be sent. Defaults
310                    to sys.stdout.
311            tracking: True to enable variable history tracking, False to
312                    disable it (default). Enabling this has a minor
313                    performance impact so typically it isn't enabled
314                    unless you need to query variable history.
315            setup_logging: True to setup a logger so that things like
316                    bb.warn() will work immediately and timeout warnings
317                    are visible; False to let BitBake do this itself.
318        """
319        self.logger = logging.getLogger('BitBake')
320        self.config_data = None
321        self.cooker = None
322        self.tracking = tracking
323        self.ui_module = None
324        self.server_connection = None
325        self.recipes_parsed = False
326        self.quiet = 0
327        self.oldhandlers = self.logger.handlers[:]
328        self.localhandlers = []
329        if setup_logging:
330            # This is the *client-side* logger, nothing to do with
331            # logging messages from the server
332            bb.msg.logger_create('BitBake', output)
333            for handler in self.logger.handlers:
334                if handler not in self.oldhandlers:
335                    self.localhandlers.append(handler)
336
337    def __enter__(self):
338        return self
339
340    def __exit__(self, type, value, traceback):
341        self.shutdown()
342
343    def prepare(self, config_only=False, config_params=None, quiet=0, extra_features=None):
344        """
345        Prepares the underlying BitBake system to be used via tinfoil.
346        This function must be called prior to calling any of the other
347        functions in the API.
348        NOTE: if you call prepare() you must absolutely call shutdown()
349        before your code terminates. You can use a "with" block to ensure
350        this happens e.g.
351
352            with bb.tinfoil.Tinfoil() as tinfoil:
353                tinfoil.prepare()
354                ...
355
356        Parameters:
357            config_only: True to read only the configuration and not load
358                        the cache / parse recipes. This is useful if you just
359                        want to query the value of a variable at the global
360                        level or you want to do anything else that doesn't
361                        involve knowing anything about the recipes in the
362                        current configuration. False loads the cache / parses
363                        recipes.
364            config_params: optionally specify your own configuration
365                        parameters. If not specified an instance of
366                        TinfoilConfigParameters will be created internally.
367            quiet:      quiet level controlling console output - equivalent
368                        to bitbake's -q/--quiet option. Default of 0 gives
369                        the same output level as normal bitbake execution.
370            extra_features: extra features to be added to the feature
371                        set requested from the server. See
372                        CookerFeatures._feature_list for possible
373                        features.
374        """
375        self.quiet = quiet
376
377        if self.tracking:
378            extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING]
379        else:
380            extrafeatures = []
381
382        if extra_features:
383            extrafeatures += extra_features
384
385        if not config_params:
386            config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet)
387
388        if not config_only:
389            # Disable local loggers because the UI module is going to set up its own
390            for handler in self.localhandlers:
391                self.logger.handlers.remove(handler)
392            self.localhandlers = []
393
394        self.server_connection, ui_module = setup_bitbake(config_params, extrafeatures)
395
396        self.ui_module = ui_module
397
398        # Ensure the path to bitbake's bin directory is in PATH so that things like
399        # bitbake-worker can be run (usually this is the case, but it doesn't have to be)
400        path = os.getenv('PATH').split(':')
401        bitbakebinpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'bin'))
402        for entry in path:
403            if entry.endswith(os.sep):
404                entry = entry[:-1]
405            if os.path.abspath(entry) == bitbakebinpath:
406                break
407        else:
408            path.insert(0, bitbakebinpath)
409            os.environ['PATH'] = ':'.join(path)
410
411        if self.server_connection:
412            _server_connections.append(self.server_connection)
413            if config_only:
414                config_params.updateToServer(self.server_connection.connection, os.environ.copy())
415                self.run_command('parseConfiguration')
416            else:
417                self.run_actions(config_params)
418                self.recipes_parsed = True
419
420            self.config_data = TinfoilDataStoreConnector(self, 0)
421            self.cooker = TinfoilCookerAdapter(self)
422            self.cooker_data = self.cooker.recipecaches['']
423        else:
424            raise Exception('Failed to start bitbake server')
425
426    def run_actions(self, config_params):
427        """
428        Run the actions specified in config_params through the UI.
429        """
430        ret = self.ui_module.main(self.server_connection.connection, self.server_connection.events, config_params)
431        if ret:
432            raise TinfoilUIException(ret)
433
434    def parseRecipes(self):
435        """
436        Legacy function - use parse_recipes() instead.
437        """
438        self.parse_recipes()
439
440    def parse_recipes(self):
441        """
442        Load information on all recipes. Normally you should specify
443        config_only=False when calling prepare() instead of using this
444        function; this function is designed for situations where you need
445        to initialise Tinfoil and use it with config_only=True first and
446        then conditionally call this function to parse recipes later.
447        """
448        config_params = TinfoilConfigParameters(config_only=False, quiet=self.quiet)
449        self.run_actions(config_params)
450        self.recipes_parsed = True
451
452    def modified_files(self):
453        """
454        Notify the server it needs to revalidate it's caches since the client has modified files
455        """
456        self.run_command("revalidateCaches")
457
458    def run_command(self, command, *params, handle_events=True):
459        """
460        Run a command on the server (as implemented in bb.command).
461        Note that there are two types of command - synchronous and
462        asynchronous; in order to receive the results of asynchronous
463        commands you will need to set an appropriate event mask
464        using set_event_mask() and listen for the result using
465        wait_event() - with the correct event mask you'll at least get
466        bb.command.CommandCompleted and possibly other events before
467        that depending on the command.
468        """
469        if not self.server_connection:
470            raise Exception('Not connected to server (did you call .prepare()?)')
471
472        commandline = [command]
473        if params:
474            commandline.extend(params)
475        try:
476            result = self.server_connection.connection.runCommand(commandline)
477        finally:
478            while handle_events:
479                event = self.wait_event()
480                if not event:
481                    break
482                if isinstance(event, logging.LogRecord):
483                    if event.taskpid == 0 or event.levelno > logging.INFO:
484                        self.logger.handle(event)
485        if result[1]:
486            raise TinfoilCommandFailed(result[1])
487        return result[0]
488
489    def set_event_mask(self, eventlist):
490        """Set the event mask which will be applied within wait_event()"""
491        if not self.server_connection:
492            raise Exception('Not connected to server (did you call .prepare()?)')
493        llevel, debug_domains = bb.msg.constructLogOptions()
494        ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
495        if not ret:
496            raise Exception('setEventMask failed')
497
498    def wait_event(self, timeout=0):
499        """
500        Wait for an event from the server for the specified time.
501        A timeout of 0 means don't wait if there are no events in the queue.
502        Returns the next event in the queue or None if the timeout was
503        reached. Note that in order to receive any events you will
504        first need to set the internal event mask using set_event_mask()
505        (otherwise whatever event mask the UI set up will be in effect).
506        """
507        if not self.server_connection:
508            raise Exception('Not connected to server (did you call .prepare()?)')
509        return self.server_connection.events.waitEvent(timeout)
510
511    def get_overlayed_recipes(self, mc=''):
512        """
513        Find recipes which are overlayed (i.e. where recipes exist in multiple layers)
514        """
515        return defaultdict(list, self.run_command('getOverlayedRecipes', mc))
516
517    def get_skipped_recipes(self):
518        """
519        Find recipes which were skipped (i.e. SkipRecipe was raised
520        during parsing).
521        """
522        return OrderedDict(self.run_command('getSkippedRecipes'))
523
524    def get_all_providers(self, mc=''):
525        return defaultdict(list, self.run_command('allProviders', mc))
526
527    def find_providers(self, mc=''):
528        return self.run_command('findProviders', mc)
529
530    def find_best_provider(self, pn):
531        return self.run_command('findBestProvider', pn)
532
533    def get_runtime_providers(self, rdep):
534        return self.run_command('getRuntimeProviders', rdep)
535
536    def get_recipe_file(self, pn):
537        """
538        Get the file name for the specified recipe/target. Raises
539        bb.providers.NoProvider if there is no match or the recipe was
540        skipped.
541        """
542        best = self.find_best_provider(pn)
543        if not best or (len(best) > 3 and not best[3]):
544            skiplist = self.get_skipped_recipes()
545            taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
546            skipreasons = taskdata.get_reasons(pn)
547            if skipreasons:
548                raise bb.providers.NoProvider('%s is unavailable:\n  %s' % (pn, '  \n'.join(skipreasons)))
549            else:
550                raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
551        return best[3]
552
553    def get_file_appends(self, fn, mc=''):
554        """
555        Find the bbappends for a recipe file
556        """
557        return self.run_command('getFileAppends', fn, mc)
558
559    def all_recipes(self, mc='', sort=True):
560        """
561        Enable iterating over all recipes in the current configuration.
562        Returns an iterator over TinfoilRecipeInfo objects created on demand.
563        Parameters:
564            mc: The multiconfig, default of '' uses the main configuration.
565            sort: True to sort recipes alphabetically (default), False otherwise
566        """
567        recipecache = self.cooker.recipecaches[mc]
568        if sort:
569            recipes = sorted(recipecache.pkg_pn.items())
570        else:
571            recipes = recipecache.pkg_pn.items()
572        for pn, fns in recipes:
573            prov = self.find_best_provider(pn)
574            recipe = TinfoilRecipeInfo(recipecache,
575                                       self.config_data,
576                                       pn=pn,
577                                       fn=prov[3],
578                                       fns=fns)
579            yield recipe
580
581    def all_recipe_files(self, mc='', variants=True, preferred_only=False):
582        """
583        Enable iterating over all recipe files in the current configuration.
584        Returns an iterator over file paths.
585        Parameters:
586            mc: The multiconfig, default of '' uses the main configuration.
587            variants: True to include variants of recipes created through
588                      BBCLASSEXTEND (default) or False to exclude them
589            preferred_only: True to include only the preferred recipe where
590                      multiple exist providing the same PN, False to list
591                      all recipes
592        """
593        recipecache = self.cooker.recipecaches[mc]
594        if preferred_only:
595            files = []
596            for pn in recipecache.pkg_pn.keys():
597                prov = self.find_best_provider(pn)
598                files.append(prov[3])
599        else:
600            files = recipecache.pkg_fn.keys()
601        for fn in sorted(files):
602            if not variants and fn.startswith('virtual:'):
603                continue
604            yield fn
605
606
607    def get_recipe_info(self, pn, mc=''):
608        """
609        Get information on a specific recipe in the current configuration by name (PN).
610        Returns a TinfoilRecipeInfo object created on demand.
611        Parameters:
612            mc: The multiconfig, default of '' uses the main configuration.
613        """
614        recipecache = self.cooker.recipecaches[mc]
615        prov = self.find_best_provider(pn)
616        fn = prov[3]
617        if fn:
618            actual_pn = recipecache.pkg_fn[fn]
619            recipe = TinfoilRecipeInfo(recipecache,
620                                        self.config_data,
621                                        pn=actual_pn,
622                                        fn=fn,
623                                        fns=recipecache.pkg_pn[actual_pn])
624            return recipe
625        else:
626            return None
627
628    def parse_recipe(self, pn):
629        """
630        Parse the specified recipe and return a datastore object
631        representing the environment for the recipe.
632        """
633        fn = self.get_recipe_file(pn)
634        return self.parse_recipe_file(fn)
635
636    def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
637        """
638        Parse the specified recipe file (with or without bbappends)
639        and return a datastore object representing the environment
640        for the recipe.
641        Parameters:
642            fn: recipe file to parse - can be a file path or virtual
643                specification
644            appends: True to apply bbappends, False otherwise
645            appendlist: optional list of bbappend files to apply, if you
646                        want to filter them
647        """
648        if self.tracking:
649            # Enable history tracking just for the parse operation
650            self.run_command('enableDataTracking')
651        try:
652            if appends and appendlist == []:
653                appends = False
654            if config_data:
655                 config_data = bb.data.createCopy(config_data)
656                 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex)
657            else:
658                dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
659            if dscon:
660                return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
661            else:
662                return None
663        finally:
664            if self.tracking:
665                self.run_command('disableDataTracking')
666
667    def build_file(self, buildfile, task, internal=True):
668        """
669        Runs the specified task for just a single recipe (i.e. no dependencies).
670        This is equivalent to bitbake -b, except with the default internal=True
671        no warning about dependencies will be produced, normal info messages
672        from the runqueue will be silenced and BuildInit, BuildStarted and
673        BuildCompleted events will not be fired.
674        """
675        return self.run_command('buildFile', buildfile, task, internal)
676
677    def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None):
678        """
679        Builds the specified targets. This is equivalent to a normal invocation
680        of bitbake. Has built-in event handling which is enabled by default and
681        can be extended if needed.
682        Parameters:
683            targets:
684                One or more targets to build. Can be a list or a
685                space-separated string.
686            task:
687                The task to run; if None then the value of BB_DEFAULT_TASK
688                will be used. Default None.
689            handle_events:
690                True to handle events in a similar way to normal bitbake
691                invocation with knotty; False to return immediately (on the
692                assumption that the caller will handle the events instead).
693                Default True.
694            extra_events:
695                An optional list of events to add to the event mask (if
696                handle_events=True). If you add events here you also need
697                to specify a callback function in event_callback that will
698                handle the additional events. Default None.
699            event_callback:
700                An optional function taking a single parameter which
701                will be called first upon receiving any event (if
702                handle_events=True) so that the caller can override or
703                extend the event handling. Default None.
704        """
705        if isinstance(targets, str):
706            targets = targets.split()
707        if not task:
708            task = self.config_data.getVar('BB_DEFAULT_TASK')
709
710        if handle_events:
711            # A reasonable set of default events matching up with those we handle below
712            eventmask = [
713                        'bb.event.BuildStarted',
714                        'bb.event.BuildCompleted',
715                        'logging.LogRecord',
716                        'bb.event.NoProvider',
717                        'bb.command.CommandCompleted',
718                        'bb.command.CommandFailed',
719                        'bb.build.TaskStarted',
720                        'bb.build.TaskFailed',
721                        'bb.build.TaskSucceeded',
722                        'bb.build.TaskFailedSilent',
723                        'bb.build.TaskProgress',
724                        'bb.runqueue.runQueueTaskStarted',
725                        'bb.runqueue.sceneQueueTaskStarted',
726                        'bb.event.ProcessStarted',
727                        'bb.event.ProcessProgress',
728                        'bb.event.ProcessFinished',
729                        ]
730            if extra_events:
731                eventmask.extend(extra_events)
732            ret = self.set_event_mask(eventmask)
733
734        includelogs = self.config_data.getVar('BBINCLUDELOGS')
735        loglines = self.config_data.getVar('BBINCLUDELOGS_LINES')
736
737        ret = self.run_command('buildTargets', targets, task)
738        if handle_events:
739            lastevent = time.time()
740            result = False
741            # Borrowed from knotty, instead somewhat hackily we use the helper
742            # as the object to store "shutdown" on
743            helper = bb.ui.uihelper.BBUIHelper()
744            helper.shutdown = 0
745            parseprogress = None
746            termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet)
747            try:
748                while True:
749                    try:
750                        event = self.wait_event(0.25)
751                        if event:
752                            lastevent = time.time()
753                            if event_callback and event_callback(event):
754                                continue
755                            if helper.eventHandler(event):
756                                if isinstance(event, bb.build.TaskFailedSilent):
757                                    self.logger.warning("Logfile for failed setscene task is %s" % event.logfile)
758                                elif isinstance(event, bb.build.TaskFailed):
759                                    bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter)
760                                continue
761                            if isinstance(event, bb.event.ProcessStarted):
762                                if self.quiet > 1:
763                                    continue
764                                parseprogress = bb.ui.knotty.new_progress(event.processname, event.total)
765                                parseprogress.start(False)
766                                continue
767                            if isinstance(event, bb.event.ProcessProgress):
768                                if self.quiet > 1:
769                                    continue
770                                if parseprogress:
771                                    parseprogress.update(event.progress)
772                                else:
773                                    bb.warn("Got ProcessProgress event for something that never started?")
774                                continue
775                            if isinstance(event, bb.event.ProcessFinished):
776                                if self.quiet > 1:
777                                    continue
778                                if parseprogress:
779                                    parseprogress.finish()
780                                parseprogress = None
781                                continue
782                            if isinstance(event, bb.command.CommandCompleted):
783                                result = True
784                                break
785                            if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)):
786                                self.logger.error(str(event))
787                                result = False
788                                break
789                            if isinstance(event, logging.LogRecord):
790                                if event.taskpid == 0 or event.levelno > logging.INFO:
791                                    self.logger.handle(event)
792                                continue
793                            if isinstance(event, bb.event.NoProvider):
794                                self.logger.error(str(event))
795                                result = False
796                                break
797                        elif helper.shutdown > 1:
798                            break
799                        termfilter.updateFooter()
800                        if time.time() > (lastevent + (3*60)):
801                            if not self.run_command('ping', handle_events=False):
802                                print("\nUnable to ping server and no events, closing down...\n")
803                                return False
804                    except KeyboardInterrupt:
805                        termfilter.clearFooter()
806                        if helper.shutdown == 1:
807                            print("\nSecond Keyboard Interrupt, stopping...\n")
808                            ret = self.run_command("stateForceShutdown")
809                            if ret and ret[2]:
810                                self.logger.error("Unable to cleanly stop: %s" % ret[2])
811                        elif helper.shutdown == 0:
812                            print("\nKeyboard Interrupt, closing down...\n")
813                            interrupted = True
814                            ret = self.run_command("stateShutdown")
815                            if ret and ret[2]:
816                                self.logger.error("Unable to cleanly shutdown: %s" % ret[2])
817                        helper.shutdown = helper.shutdown + 1
818                termfilter.clearFooter()
819            finally:
820                termfilter.finish()
821            if helper.failed_tasks:
822                result = False
823            return result
824        else:
825            return ret
826
827    def shutdown(self):
828        """
829        Shut down tinfoil. Disconnects from the server and gracefully
830        releases any associated resources. You must call this function if
831        prepare() has been called, or use a with... block when you create
832        the tinfoil object which will ensure that it gets called.
833        """
834        try:
835            if self.server_connection:
836                try:
837                    self.run_command('clientComplete')
838                finally:
839                    _server_connections.remove(self.server_connection)
840                    bb.event.ui_queue = []
841                    self.server_connection.terminate()
842                    self.server_connection = None
843
844        finally:
845            # Restore logging handlers to how it looked when we started
846            if self.oldhandlers:
847                for handler in self.logger.handlers:
848                    if handler not in self.oldhandlers:
849                        self.logger.handlers.remove(handler)
850
851    def _reconvert_type(self, obj, origtypename):
852        """
853        Convert an object back to the right type, in the case
854        that marshalling has changed it (especially with xmlrpc)
855        """
856        supported_types = {
857            'set': set,
858            'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
859        }
860
861        origtype = supported_types.get(origtypename, None)
862        if origtype is None:
863            raise Exception('Unsupported type "%s"' % origtypename)
864        if type(obj) == origtype:
865            newobj = obj
866        elif isinstance(obj, dict):
867            # New style class
868            newobj = origtype()
869            for k,v in obj.items():
870                setattr(newobj, k, v)
871        else:
872            # Assume we can coerce the type
873            newobj = origtype(obj)
874
875        if isinstance(newobj, bb.command.DataStoreConnectionHandle):
876            newobj = TinfoilDataStoreConnector(self, newobj.dsindex)
877
878        return newobj
879
880
881class TinfoilConfigParameters(BitBakeConfigParameters):
882
883    def __init__(self, config_only, **options):
884        self.initial_options = options
885        # Apply some sane defaults
886        if not 'parse_only' in options:
887            self.initial_options['parse_only'] = not config_only
888        #if not 'status_only' in options:
889        #    self.initial_options['status_only'] = config_only
890        if not 'ui' in options:
891            self.initial_options['ui'] = 'knotty'
892        if not 'argv' in options:
893            self.initial_options['argv'] = []
894
895        super(TinfoilConfigParameters, self).__init__()
896
897    def parseCommandLine(self, argv=None):
898        # We don't want any parameters parsed from the command line
899        opts = super(TinfoilConfigParameters, self).parseCommandLine([])
900        for key, val in self.initial_options.items():
901            setattr(opts[0], key, val)
902        return opts
903