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