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