xref: /openbmc/openbmc/poky/bitbake/lib/bb/command.py (revision 82c905dc)
1"""
2BitBake 'Command' module
3
4Provide an interface to interact with the bitbake server through 'commands'
5"""
6
7# Copyright (C) 2006-2007  Richard Purdie
8#
9# SPDX-License-Identifier: GPL-2.0-only
10#
11
12"""
13The bitbake server takes 'commands' from its UI/commandline.
14Commands are either synchronous or asynchronous.
15Async commands return data to the client in the form of events.
16Sync commands must only return data through the function return value
17and must not trigger events, directly or indirectly.
18Commands are queued in a CommandQueue
19"""
20
21from collections import OrderedDict, defaultdict
22
23import bb.event
24import bb.cooker
25import bb.remotedata
26
27class DataStoreConnectionHandle(object):
28    def __init__(self, dsindex=0):
29        self.dsindex = dsindex
30
31class CommandCompleted(bb.event.Event):
32    pass
33
34class CommandExit(bb.event.Event):
35    def  __init__(self, exitcode):
36        bb.event.Event.__init__(self)
37        self.exitcode = int(exitcode)
38
39class CommandFailed(CommandExit):
40    def __init__(self, message):
41        self.error = message
42        CommandExit.__init__(self, 1)
43    def __str__(self):
44        return "Command execution failed: %s" % self.error
45
46class CommandError(Exception):
47    pass
48
49class Command:
50    """
51    A queue of asynchronous commands for bitbake
52    """
53    def __init__(self, cooker):
54        self.cooker = cooker
55        self.cmds_sync = CommandsSync()
56        self.cmds_async = CommandsAsync()
57        self.remotedatastores = bb.remotedata.RemoteDatastores(cooker)
58
59        # FIXME Add lock for this
60        self.currentAsyncCommand = None
61
62    def runCommand(self, commandline, ro_only = False):
63        command = commandline.pop(0)
64        if hasattr(CommandsSync, command):
65            # Can run synchronous commands straight away
66            command_method = getattr(self.cmds_sync, command)
67            if ro_only:
68                if not hasattr(command_method, 'readonly') or not getattr(command_method, 'readonly'):
69                    return None, "Not able to execute not readonly commands in readonly mode"
70            try:
71                self.cooker.process_inotify_updates()
72                if getattr(command_method, 'needconfig', True):
73                    self.cooker.updateCacheSync()
74                result = command_method(self, commandline)
75            except CommandError as exc:
76                return None, exc.args[0]
77            except (Exception, SystemExit):
78                import traceback
79                return None, traceback.format_exc()
80            else:
81                return result, None
82        if self.currentAsyncCommand is not None:
83            return None, "Busy (%s in progress)" % self.currentAsyncCommand[0]
84        if command not in CommandsAsync.__dict__:
85            return None, "No such command"
86        self.currentAsyncCommand = (command, commandline)
87        self.cooker.configuration.server_register_idlecallback(self.cooker.runCommands, self.cooker)
88        return True, None
89
90    def runAsyncCommand(self):
91        try:
92            self.cooker.process_inotify_updates()
93            if self.cooker.state in (bb.cooker.state.error, bb.cooker.state.shutdown, bb.cooker.state.forceshutdown):
94                # updateCache will trigger a shutdown of the parser
95                # and then raise BBHandledException triggering an exit
96                self.cooker.updateCache()
97                return False
98            if self.currentAsyncCommand is not None:
99                (command, options) = self.currentAsyncCommand
100                commandmethod = getattr(CommandsAsync, command)
101                needcache = getattr( commandmethod, "needcache" )
102                if needcache and self.cooker.state != bb.cooker.state.running:
103                    self.cooker.updateCache()
104                    return True
105                else:
106                    commandmethod(self.cmds_async, self, options)
107                    return False
108            else:
109                return False
110        except KeyboardInterrupt as exc:
111            self.finishAsyncCommand("Interrupted")
112            return False
113        except SystemExit as exc:
114            arg = exc.args[0]
115            if isinstance(arg, str):
116                self.finishAsyncCommand(arg)
117            else:
118                self.finishAsyncCommand("Exited with %s" % arg)
119            return False
120        except Exception as exc:
121            import traceback
122            if isinstance(exc, bb.BBHandledException):
123                self.finishAsyncCommand("")
124            else:
125                self.finishAsyncCommand(traceback.format_exc())
126            return False
127
128    def finishAsyncCommand(self, msg=None, code=None):
129        if msg or msg == "":
130            bb.event.fire(CommandFailed(msg), self.cooker.data)
131        elif code:
132            bb.event.fire(CommandExit(code), self.cooker.data)
133        else:
134            bb.event.fire(CommandCompleted(), self.cooker.data)
135        self.currentAsyncCommand = None
136        self.cooker.finishcommand()
137
138    def reset(self):
139        self.remotedatastores = bb.remotedata.RemoteDatastores(self.cooker)
140
141def split_mc_pn(pn):
142    if pn.startswith("multiconfig:"):
143        _, mc, pn = pn.split(":", 2)
144        return (mc, pn)
145    return ('', pn)
146
147class CommandsSync:
148    """
149    A class of synchronous commands
150    These should run quickly so as not to hurt interactive performance.
151    These must not influence any running synchronous command.
152    """
153
154    def stateShutdown(self, command, params):
155        """
156        Trigger cooker 'shutdown' mode
157        """
158        command.cooker.shutdown(False)
159
160    def stateForceShutdown(self, command, params):
161        """
162        Stop the cooker
163        """
164        command.cooker.shutdown(True)
165
166    def getAllKeysWithFlags(self, command, params):
167        """
168        Returns a dump of the global state. Call with
169        variable flags to be retrieved as params.
170        """
171        flaglist = params[0]
172        return command.cooker.getAllKeysWithFlags(flaglist)
173    getAllKeysWithFlags.readonly = True
174
175    def getVariable(self, command, params):
176        """
177        Read the value of a variable from data
178        """
179        varname = params[0]
180        expand = True
181        if len(params) > 1:
182            expand = (params[1] == "True")
183
184        return command.cooker.data.getVar(varname, expand)
185    getVariable.readonly = True
186
187    def setVariable(self, command, params):
188        """
189        Set the value of variable in data
190        """
191        varname = params[0]
192        value = str(params[1])
193        command.cooker.extraconfigdata[varname] = value
194        command.cooker.data.setVar(varname, value)
195
196    def getSetVariable(self, command, params):
197        """
198        Read the value of a variable from data and set it into the datastore
199        which effectively expands and locks the value.
200        """
201        varname = params[0]
202        result = self.getVariable(command, params)
203        command.cooker.data.setVar(varname, result)
204        return result
205
206    def setConfig(self, command, params):
207        """
208        Set the value of variable in configuration
209        """
210        varname = params[0]
211        value = str(params[1])
212        setattr(command.cooker.configuration, varname, value)
213
214    def enableDataTracking(self, command, params):
215        """
216        Enable history tracking for variables
217        """
218        command.cooker.enableDataTracking()
219
220    def disableDataTracking(self, command, params):
221        """
222        Disable history tracking for variables
223        """
224        command.cooker.disableDataTracking()
225
226    def setPrePostConfFiles(self, command, params):
227        prefiles = params[0].split()
228        postfiles = params[1].split()
229        command.cooker.configuration.prefile = prefiles
230        command.cooker.configuration.postfile = postfiles
231    setPrePostConfFiles.needconfig = False
232
233    def matchFile(self, command, params):
234        fMatch = params[0]
235        return command.cooker.matchFile(fMatch)
236    matchFile.needconfig = False
237
238    def getUIHandlerNum(self, command, params):
239        return bb.event.get_uihandler()
240    getUIHandlerNum.needconfig = False
241    getUIHandlerNum.readonly = True
242
243    def setEventMask(self, command, params):
244        handlerNum = params[0]
245        llevel = params[1]
246        debug_domains = params[2]
247        mask = params[3]
248        return bb.event.set_UIHmask(handlerNum, llevel, debug_domains, mask)
249    setEventMask.needconfig = False
250    setEventMask.readonly = True
251
252    def setFeatures(self, command, params):
253        """
254        Set the cooker features to include the passed list of features
255        """
256        features = params[0]
257        command.cooker.setFeatures(features)
258    setFeatures.needconfig = False
259    # although we change the internal state of the cooker, this is transparent since
260    # we always take and leave the cooker in state.initial
261    setFeatures.readonly = True
262
263    def updateConfig(self, command, params):
264        options = params[0]
265        environment = params[1]
266        cmdline = params[2]
267        command.cooker.updateConfigOpts(options, environment, cmdline)
268    updateConfig.needconfig = False
269
270    def parseConfiguration(self, command, params):
271        """Instruct bitbake to parse its configuration
272        NOTE: it is only necessary to call this if you aren't calling any normal action
273        (otherwise parsing is taken care of automatically)
274        """
275        command.cooker.parseConfiguration()
276    parseConfiguration.needconfig = False
277
278    def getLayerPriorities(self, command, params):
279        command.cooker.parseConfiguration()
280        ret = []
281        # regex objects cannot be marshalled by xmlrpc
282        for collection, pattern, regex, pri in command.cooker.bbfile_config_priorities:
283            ret.append((collection, pattern, regex.pattern, pri))
284        return ret
285    getLayerPriorities.readonly = True
286
287    def getRecipes(self, command, params):
288        try:
289            mc = params[0]
290        except IndexError:
291            mc = ''
292        return list(command.cooker.recipecaches[mc].pkg_pn.items())
293    getRecipes.readonly = True
294
295    def getRecipeDepends(self, command, params):
296        try:
297            mc = params[0]
298        except IndexError:
299            mc = ''
300        return list(command.cooker.recipecaches[mc].deps.items())
301    getRecipeDepends.readonly = True
302
303    def getRecipeVersions(self, command, params):
304        try:
305            mc = params[0]
306        except IndexError:
307            mc = ''
308        return command.cooker.recipecaches[mc].pkg_pepvpr
309    getRecipeVersions.readonly = True
310
311    def getRecipeProvides(self, command, params):
312        try:
313            mc = params[0]
314        except IndexError:
315            mc = ''
316        return command.cooker.recipecaches[mc].fn_provides
317    getRecipeProvides.readonly = True
318
319    def getRecipePackages(self, command, params):
320        try:
321            mc = params[0]
322        except IndexError:
323            mc = ''
324        return command.cooker.recipecaches[mc].packages
325    getRecipePackages.readonly = True
326
327    def getRecipePackagesDynamic(self, command, params):
328        try:
329            mc = params[0]
330        except IndexError:
331            mc = ''
332        return command.cooker.recipecaches[mc].packages_dynamic
333    getRecipePackagesDynamic.readonly = True
334
335    def getRProviders(self, command, params):
336        try:
337            mc = params[0]
338        except IndexError:
339            mc = ''
340        return command.cooker.recipecaches[mc].rproviders
341    getRProviders.readonly = True
342
343    def getRuntimeDepends(self, command, params):
344        ret = []
345        try:
346            mc = params[0]
347        except IndexError:
348            mc = ''
349        rundeps = command.cooker.recipecaches[mc].rundeps
350        for key, value in rundeps.items():
351            if isinstance(value, defaultdict):
352                value = dict(value)
353            ret.append((key, value))
354        return ret
355    getRuntimeDepends.readonly = True
356
357    def getRuntimeRecommends(self, command, params):
358        ret = []
359        try:
360            mc = params[0]
361        except IndexError:
362            mc = ''
363        runrecs = command.cooker.recipecaches[mc].runrecs
364        for key, value in runrecs.items():
365            if isinstance(value, defaultdict):
366                value = dict(value)
367            ret.append((key, value))
368        return ret
369    getRuntimeRecommends.readonly = True
370
371    def getRecipeInherits(self, command, params):
372        try:
373            mc = params[0]
374        except IndexError:
375            mc = ''
376        return command.cooker.recipecaches[mc].inherits
377    getRecipeInherits.readonly = True
378
379    def getBbFilePriority(self, command, params):
380        try:
381            mc = params[0]
382        except IndexError:
383            mc = ''
384        return command.cooker.recipecaches[mc].bbfile_priority
385    getBbFilePriority.readonly = True
386
387    def getDefaultPreference(self, command, params):
388        try:
389            mc = params[0]
390        except IndexError:
391            mc = ''
392        return command.cooker.recipecaches[mc].pkg_dp
393    getDefaultPreference.readonly = True
394
395    def getSkippedRecipes(self, command, params):
396        # Return list sorted by reverse priority order
397        import bb.cache
398        skipdict = OrderedDict(sorted(command.cooker.skiplist.items(),
399                                      key=lambda x: (-command.cooker.collection.calc_bbfile_priority(bb.cache.virtualfn2realfn(x[0])[0]), x[0])))
400        return list(skipdict.items())
401    getSkippedRecipes.readonly = True
402
403    def getOverlayedRecipes(self, command, params):
404        return list(command.cooker.collection.overlayed.items())
405    getOverlayedRecipes.readonly = True
406
407    def getFileAppends(self, command, params):
408        fn = params[0]
409        return command.cooker.collection.get_file_appends(fn)
410    getFileAppends.readonly = True
411
412    def getAllAppends(self, command, params):
413        return command.cooker.collection.bbappends
414    getAllAppends.readonly = True
415
416    def findProviders(self, command, params):
417        try:
418            mc = params[0]
419        except IndexError:
420            mc = ''
421        return command.cooker.findProviders(mc)
422    findProviders.readonly = True
423
424    def findBestProvider(self, command, params):
425        (mc, pn) = split_mc_pn(params[0])
426        return command.cooker.findBestProvider(pn, mc)
427    findBestProvider.readonly = True
428
429    def allProviders(self, command, params):
430        try:
431            mc = params[0]
432        except IndexError:
433            mc = ''
434        return list(bb.providers.allProviders(command.cooker.recipecaches[mc]).items())
435    allProviders.readonly = True
436
437    def getRuntimeProviders(self, command, params):
438        rprovide = params[0]
439        try:
440            mc = params[1]
441        except IndexError:
442            mc = ''
443        all_p = bb.providers.getRuntimeProviders(command.cooker.recipecaches[mc], rprovide)
444        if all_p:
445            best = bb.providers.filterProvidersRunTime(all_p, rprovide,
446                            command.cooker.data,
447                            command.cooker.recipecaches[mc])[0][0]
448        else:
449            best = None
450        return all_p, best
451    getRuntimeProviders.readonly = True
452
453    def dataStoreConnectorCmd(self, command, params):
454        dsindex = params[0]
455        method = params[1]
456        args = params[2]
457        kwargs = params[3]
458
459        d = command.remotedatastores[dsindex]
460        ret = getattr(d, method)(*args, **kwargs)
461
462        if isinstance(ret, bb.data_smart.DataSmart):
463            idx = command.remotedatastores.store(ret)
464            return DataStoreConnectionHandle(idx)
465
466        return ret
467
468    def dataStoreConnectorVarHistCmd(self, command, params):
469        dsindex = params[0]
470        method = params[1]
471        args = params[2]
472        kwargs = params[3]
473
474        d = command.remotedatastores[dsindex].varhistory
475        return getattr(d, method)(*args, **kwargs)
476
477    def dataStoreConnectorIncHistCmd(self, command, params):
478        dsindex = params[0]
479        method = params[1]
480        args = params[2]
481        kwargs = params[3]
482
483        d = command.remotedatastores[dsindex].inchistory
484        return getattr(d, method)(*args, **kwargs)
485
486    def dataStoreConnectorRelease(self, command, params):
487        dsindex = params[0]
488        if dsindex <= 0:
489            raise CommandError('dataStoreConnectorRelease: invalid index %d' % dsindex)
490        command.remotedatastores.release(dsindex)
491
492    def parseRecipeFile(self, command, params):
493        """
494        Parse the specified recipe file (with or without bbappends)
495        and return a datastore object representing the environment
496        for the recipe.
497        """
498        fn = params[0]
499        appends = params[1]
500        appendlist = params[2]
501        if len(params) > 3:
502            config_data = command.remotedatastores[params[3]]
503        else:
504            config_data = None
505
506        if appends:
507            if appendlist is not None:
508                appendfiles = appendlist
509            else:
510                appendfiles = command.cooker.collection.get_file_appends(fn)
511        else:
512            appendfiles = []
513        # We are calling bb.cache locally here rather than on the server,
514        # but that's OK because it doesn't actually need anything from
515        # the server barring the global datastore (which we have a remote
516        # version of)
517        if config_data:
518            # We have to use a different function here if we're passing in a datastore
519            # NOTE: we took a copy above, so we don't do it here again
520            envdata = bb.cache.parse_recipe(config_data, fn, appendfiles)['']
521        else:
522            # Use the standard path
523            parser = bb.cache.NoCache(command.cooker.databuilder)
524            envdata = parser.loadDataFull(fn, appendfiles)
525        idx = command.remotedatastores.store(envdata)
526        return DataStoreConnectionHandle(idx)
527    parseRecipeFile.readonly = True
528
529class CommandsAsync:
530    """
531    A class of asynchronous commands
532    These functions communicate via generated events.
533    Any function that requires metadata parsing should be here.
534    """
535
536    def buildFile(self, command, params):
537        """
538        Build a single specified .bb file
539        """
540        bfile = params[0]
541        task = params[1]
542        if len(params) > 2:
543            internal = params[2]
544        else:
545            internal = False
546
547        if internal:
548            command.cooker.buildFileInternal(bfile, task, fireevents=False, quietlog=True)
549        else:
550            command.cooker.buildFile(bfile, task)
551    buildFile.needcache = False
552
553    def buildTargets(self, command, params):
554        """
555        Build a set of targets
556        """
557        pkgs_to_build = params[0]
558        task = params[1]
559
560        command.cooker.buildTargets(pkgs_to_build, task)
561    buildTargets.needcache = True
562
563    def generateDepTreeEvent(self, command, params):
564        """
565        Generate an event containing the dependency information
566        """
567        pkgs_to_build = params[0]
568        task = params[1]
569
570        command.cooker.generateDepTreeEvent(pkgs_to_build, task)
571        command.finishAsyncCommand()
572    generateDepTreeEvent.needcache = True
573
574    def generateDotGraph(self, command, params):
575        """
576        Dump dependency information to disk as .dot files
577        """
578        pkgs_to_build = params[0]
579        task = params[1]
580
581        command.cooker.generateDotGraphFiles(pkgs_to_build, task)
582        command.finishAsyncCommand()
583    generateDotGraph.needcache = True
584
585    def generateTargetsTree(self, command, params):
586        """
587        Generate a tree of buildable targets.
588        If klass is provided ensure all recipes that inherit the class are
589        included in the package list.
590        If pkg_list provided use that list (plus any extras brought in by
591        klass) rather than generating a tree for all packages.
592        """
593        klass = params[0]
594        pkg_list = params[1]
595
596        command.cooker.generateTargetsTree(klass, pkg_list)
597        command.finishAsyncCommand()
598    generateTargetsTree.needcache = True
599
600    def findConfigFiles(self, command, params):
601        """
602        Find config files which provide appropriate values
603        for the passed configuration variable. i.e. MACHINE
604        """
605        varname = params[0]
606
607        command.cooker.findConfigFiles(varname)
608        command.finishAsyncCommand()
609    findConfigFiles.needcache = False
610
611    def findFilesMatchingInDir(self, command, params):
612        """
613        Find implementation files matching the specified pattern
614        in the requested subdirectory of a BBPATH
615        """
616        pattern = params[0]
617        directory = params[1]
618
619        command.cooker.findFilesMatchingInDir(pattern, directory)
620        command.finishAsyncCommand()
621    findFilesMatchingInDir.needcache = False
622
623    def findConfigFilePath(self, command, params):
624        """
625        Find the path of the requested configuration file
626        """
627        configfile = params[0]
628
629        command.cooker.findConfigFilePath(configfile)
630        command.finishAsyncCommand()
631    findConfigFilePath.needcache = False
632
633    def showVersions(self, command, params):
634        """
635        Show the currently selected versions
636        """
637        command.cooker.showVersions()
638        command.finishAsyncCommand()
639    showVersions.needcache = True
640
641    def showEnvironmentTarget(self, command, params):
642        """
643        Print the environment of a target recipe
644        (needs the cache to work out which recipe to use)
645        """
646        pkg = params[0]
647
648        command.cooker.showEnvironment(None, pkg)
649        command.finishAsyncCommand()
650    showEnvironmentTarget.needcache = True
651
652    def showEnvironment(self, command, params):
653        """
654        Print the standard environment
655        or if specified the environment for a specified recipe
656        """
657        bfile = params[0]
658
659        command.cooker.showEnvironment(bfile)
660        command.finishAsyncCommand()
661    showEnvironment.needcache = False
662
663    def parseFiles(self, command, params):
664        """
665        Parse the .bb files
666        """
667        command.cooker.updateCache()
668        command.finishAsyncCommand()
669    parseFiles.needcache = True
670
671    def compareRevisions(self, command, params):
672        """
673        Parse the .bb files
674        """
675        if bb.fetch.fetcher_compare_revisions(command.cooker.data):
676            command.finishAsyncCommand(code=1)
677        else:
678            command.finishAsyncCommand()
679    compareRevisions.needcache = True
680
681    def triggerEvent(self, command, params):
682        """
683        Trigger a certain event
684        """
685        event = params[0]
686        bb.event.fire(eval(event), command.cooker.data)
687        command.currentAsyncCommand = None
688    triggerEvent.needcache = False
689
690    def resetCooker(self, command, params):
691        """
692        Reset the cooker to its initial state, thus forcing a reparse for
693        any async command that has the needcache property set to True
694        """
695        command.cooker.reset()
696        command.finishAsyncCommand()
697    resetCooker.needcache = False
698
699    def clientComplete(self, command, params):
700        """
701        Do the right thing when the controlling client exits
702        """
703        command.cooker.clientComplete()
704        command.finishAsyncCommand()
705    clientComplete.needcache = False
706
707    def findSigInfo(self, command, params):
708        """
709        Find signature info files via the signature generator
710        """
711        pn = params[0]
712        taskname = params[1]
713        sigs = params[2]
714        res = bb.siggen.find_siginfo(pn, taskname, sigs, command.cooker.data)
715        bb.event.fire(bb.event.FindSigInfoResult(res), command.cooker.data)
716        command.finishAsyncCommand()
717    findSigInfo.needcache = False
718