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