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