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