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