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