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