blob: 8c9b6b8ca5c66739fad7c8762eea100dcc743423 [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
Andrew Geissler82c905d2020-04-13 13:39:40 -050016from functools import partial
Patrick Williamsc124f4f2015-09-15 14:41:29 -050017
18import bb.cache
19import bb.cooker
20import bb.providers
Brad Bishop6e60e8b2018-02-01 10:27:11 -050021import bb.taskdata
Patrick Williamsc124f4f2015-09-15 14:41:29 -050022import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -050023import bb.command
24import bb.remotedata
Andrew Geissler82c905d2020-04-13 13:39:40 -050025from bb.cookerdata import CookerConfiguration
26from bb.main import setup_bitbake, BitBakeConfigParameters
Patrick Williamsc124f4f2015-09-15 14:41:29 -050027import bb.fetch2
28
Brad Bishop6e60e8b2018-02-01 10:27:11 -050029
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
Andrew Geissler82c905d2020-04-13 13:39:40 -050048class TinfoilDataStoreConnectorVarHistory:
49 def __init__(self, tinfoil, dsindex):
50 self.tinfoil = tinfoil
51 self.dsindex = dsindex
52
53 def remoteCommand(self, cmd, *args, **kwargs):
54 return self.tinfoil.run_command('dataStoreConnectorVarHistCmd', self.dsindex, cmd, args, kwargs)
55
56 def __getattr__(self, name):
57 if not hasattr(bb.data_smart.VariableHistory, name):
58 raise AttributeError("VariableHistory has no such method %s" % name)
59
60 newfunc = partial(self.remoteCommand, name)
61 setattr(self, name, newfunc)
62 return newfunc
63
64class TinfoilDataStoreConnectorIncHistory:
65 def __init__(self, tinfoil, dsindex):
66 self.tinfoil = tinfoil
67 self.dsindex = dsindex
68
69 def remoteCommand(self, cmd, *args, **kwargs):
70 return self.tinfoil.run_command('dataStoreConnectorIncHistCmd', self.dsindex, cmd, args, kwargs)
71
72 def __getattr__(self, name):
73 if not hasattr(bb.data_smart.IncludeHistory, name):
74 raise AttributeError("IncludeHistory has no such method %s" % name)
75
76 newfunc = partial(self.remoteCommand, name)
77 setattr(self, name, newfunc)
78 return newfunc
79
Brad Bishop6e60e8b2018-02-01 10:27:11 -050080class TinfoilDataStoreConnector:
Andrew Geissler82c905d2020-04-13 13:39:40 -050081 """
82 Connector object used to enable access to datastore objects via tinfoil
83 Method calls are transmitted to the remote datastore for processing, if a datastore is
84 returned we return a connector object for the new store
85 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -050086
87 def __init__(self, tinfoil, dsindex):
88 self.tinfoil = tinfoil
89 self.dsindex = dsindex
Andrew Geissler82c905d2020-04-13 13:39:40 -050090 self.varhistory = TinfoilDataStoreConnectorVarHistory(tinfoil, dsindex)
91 self.inchistory = TinfoilDataStoreConnectorIncHistory(tinfoil, dsindex)
92
93 def remoteCommand(self, cmd, *args, **kwargs):
94 ret = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, cmd, args, kwargs)
95 if isinstance(ret, bb.command.DataStoreConnectionHandle):
96 return TinfoilDataStoreConnector(self.tinfoil, ret.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -050097 return ret
Andrew Geissler82c905d2020-04-13 13:39:40 -050098
99 def __getattr__(self, name):
100 if not hasattr(bb.data._dict_type, name):
101 raise AttributeError("Data store has no such method %s" % name)
102
103 newfunc = partial(self.remoteCommand, name)
104 setattr(self, name, newfunc)
105 return newfunc
106
107 def __iter__(self):
108 keys = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, "keys", [], {})
109 for k in keys:
110 yield k
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500111
112class TinfoilCookerAdapter:
113 """
114 Provide an adapter for existing code that expects to access a cooker object via Tinfoil,
115 since now Tinfoil is on the client side it no longer has direct access.
116 """
117
118 class TinfoilCookerCollectionAdapter:
119 """ cooker.collection adapter """
120 def __init__(self, tinfoil):
121 self.tinfoil = tinfoil
122 def get_file_appends(self, fn):
123 return self.tinfoil.get_file_appends(fn)
124 def __getattr__(self, name):
125 if name == 'overlayed':
126 return self.tinfoil.get_overlayed_recipes()
127 elif name == 'bbappends':
128 return self.tinfoil.run_command('getAllAppends')
129 else:
130 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
131
132 class TinfoilRecipeCacheAdapter:
133 """ cooker.recipecache adapter """
Andrew Geissler82c905d2020-04-13 13:39:40 -0500134 def __init__(self, tinfoil, mc=''):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500135 self.tinfoil = tinfoil
Andrew Geissler82c905d2020-04-13 13:39:40 -0500136 self.mc = mc
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500137 self._cache = {}
138
139 def get_pkg_pn_fn(self):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500140 pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500141 pkg_fn = {}
142 for pn, fnlist in pkg_pn.items():
143 for fn in fnlist:
144 pkg_fn[fn] = pn
145 self._cache['pkg_pn'] = pkg_pn
146 self._cache['pkg_fn'] = pkg_fn
147
148 def __getattr__(self, name):
149 # Grab these only when they are requested since they aren't always used
150 if name in self._cache:
151 return self._cache[name]
152 elif name == 'pkg_pn':
153 self.get_pkg_pn_fn()
154 return self._cache[name]
155 elif name == 'pkg_fn':
156 self.get_pkg_pn_fn()
157 return self._cache[name]
158 elif name == 'deps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500159 attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500160 elif name == 'rundeps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500161 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500162 elif name == 'runrecs':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500163 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500164 elif name == 'pkg_pepvpr':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500165 attrvalue = self.tinfoil.run_command('getRecipeVersions', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500166 elif name == 'inherits':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500167 attrvalue = self.tinfoil.run_command('getRecipeInherits', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500168 elif name == 'bbfile_priority':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500169 attrvalue = self.tinfoil.run_command('getBbFilePriority', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500170 elif name == 'pkg_dp':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500171 attrvalue = self.tinfoil.run_command('getDefaultPreference', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500172 elif name == 'fn_provides':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500173 attrvalue = self.tinfoil.run_command('getRecipeProvides', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500174 elif name == 'packages':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500175 attrvalue = self.tinfoil.run_command('getRecipePackages', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500176 elif name == 'packages_dynamic':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500177 attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500178 elif name == 'rproviders':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500179 attrvalue = self.tinfoil.run_command('getRProviders', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500180 else:
181 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
182
183 self._cache[name] = attrvalue
184 return attrvalue
185
186 def __init__(self, tinfoil):
187 self.tinfoil = tinfoil
188 self.collection = self.TinfoilCookerCollectionAdapter(tinfoil)
189 self.recipecaches = {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500190 self.recipecaches[''] = self.TinfoilRecipeCacheAdapter(tinfoil)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500191 for mc in (tinfoil.config_data.getVar('BBMULTICONFIG') or '').split():
192 self.recipecaches[mc] = self.TinfoilRecipeCacheAdapter(tinfoil, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500193 self._cache = {}
194 def __getattr__(self, name):
195 # Grab these only when they are requested since they aren't always used
196 if name in self._cache:
197 return self._cache[name]
198 elif name == 'skiplist':
199 attrvalue = self.tinfoil.get_skipped_recipes()
200 elif name == 'bbfile_config_priorities':
201 ret = self.tinfoil.run_command('getLayerPriorities')
202 bbfile_config_priorities = []
203 for collection, pattern, regex, pri in ret:
204 bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri))
205
206 attrvalue = bbfile_config_priorities
207 else:
208 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
209
210 self._cache[name] = attrvalue
211 return attrvalue
212
213 def findBestProvider(self, pn):
214 return self.tinfoil.find_best_provider(pn)
215
216
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500217class TinfoilRecipeInfo:
218 """
219 Provides a convenient representation of the cached information for a single recipe.
220 Some attributes are set on construction, others are read on-demand (which internally
221 may result in a remote procedure call to the bitbake server the first time).
222 Note that only information which is cached is available through this object - if
223 you need other variable values you will need to parse the recipe using
224 Tinfoil.parse_recipe().
225 """
226 def __init__(self, recipecache, d, pn, fn, fns):
227 self._recipecache = recipecache
228 self._d = d
229 self.pn = pn
230 self.fn = fn
231 self.fns = fns
232 self.inherit_files = recipecache.inherits[fn]
233 self.depends = recipecache.deps[fn]
234 (self.pe, self.pv, self.pr) = recipecache.pkg_pepvpr[fn]
235 self._cached_packages = None
236 self._cached_rprovides = None
237 self._cached_packages_dynamic = None
238
239 def __getattr__(self, name):
240 if name == 'alternates':
241 return [x for x in self.fns if x != self.fn]
242 elif name == 'rdepends':
243 return self._recipecache.rundeps[self.fn]
244 elif name == 'rrecommends':
245 return self._recipecache.runrecs[self.fn]
246 elif name == 'provides':
247 return self._recipecache.fn_provides[self.fn]
248 elif name == 'packages':
249 if self._cached_packages is None:
250 self._cached_packages = []
251 for pkg, fns in self._recipecache.packages.items():
252 if self.fn in fns:
253 self._cached_packages.append(pkg)
254 return self._cached_packages
255 elif name == 'packages_dynamic':
256 if self._cached_packages_dynamic is None:
257 self._cached_packages_dynamic = []
258 for pkg, fns in self._recipecache.packages_dynamic.items():
259 if self.fn in fns:
260 self._cached_packages_dynamic.append(pkg)
261 return self._cached_packages_dynamic
262 elif name == 'rprovides':
263 if self._cached_rprovides is None:
264 self._cached_rprovides = []
265 for pkg, fns in self._recipecache.rproviders.items():
266 if self.fn in fns:
267 self._cached_rprovides.append(pkg)
268 return self._cached_rprovides
269 else:
270 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
271 def inherits(self, only_recipe=False):
272 """
273 Get the inherited classes for a recipe. Returns the class names only.
274 Parameters:
275 only_recipe: True to return only the classes inherited by the recipe
276 itself, False to return all classes inherited within
277 the context for the recipe (which includes globally
278 inherited classes).
279 """
280 if only_recipe:
281 global_inherit = [x for x in (self._d.getVar('BBINCLUDED') or '').split() if x.endswith('.bbclass')]
282 else:
283 global_inherit = []
284 for clsfile in self.inherit_files:
285 if only_recipe and clsfile in global_inherit:
286 continue
287 clsname = os.path.splitext(os.path.basename(clsfile))[0]
288 yield clsname
289 def __str__(self):
290 return '%s' % self.pn
291
292
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500293class Tinfoil:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500294 """
295 Tinfoil - an API for scripts and utilities to query
296 BitBake internals and perform build operations.
297 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500298
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500299 def __init__(self, output=sys.stdout, tracking=False, setup_logging=True):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500300 """
301 Create a new tinfoil object.
302 Parameters:
303 output: specifies where console output should be sent. Defaults
304 to sys.stdout.
305 tracking: True to enable variable history tracking, False to
306 disable it (default). Enabling this has a minor
307 performance impact so typically it isn't enabled
308 unless you need to query variable history.
309 setup_logging: True to setup a logger so that things like
310 bb.warn() will work immediately and timeout warnings
311 are visible; False to let BitBake do this itself.
312 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500313 self.logger = logging.getLogger('BitBake')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500314 self.config_data = None
315 self.cooker = None
316 self.tracking = tracking
317 self.ui_module = None
318 self.server_connection = None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500319 self.recipes_parsed = False
320 self.quiet = 0
321 self.oldhandlers = self.logger.handlers[:]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500322 if setup_logging:
323 # This is the *client-side* logger, nothing to do with
324 # logging messages from the server
325 bb.msg.logger_create('BitBake', output)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500326 self.localhandlers = []
327 for handler in self.logger.handlers:
328 if handler not in self.oldhandlers:
329 self.localhandlers.append(handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500330
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600331 def __enter__(self):
332 return self
333
334 def __exit__(self, type, value, traceback):
335 self.shutdown()
336
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500337 def prepare(self, config_only=False, config_params=None, quiet=0, extra_features=None):
338 """
339 Prepares the underlying BitBake system to be used via tinfoil.
340 This function must be called prior to calling any of the other
341 functions in the API.
342 NOTE: if you call prepare() you must absolutely call shutdown()
343 before your code terminates. You can use a "with" block to ensure
344 this happens e.g.
345
346 with bb.tinfoil.Tinfoil() as tinfoil:
347 tinfoil.prepare()
348 ...
349
350 Parameters:
351 config_only: True to read only the configuration and not load
352 the cache / parse recipes. This is useful if you just
353 want to query the value of a variable at the global
354 level or you want to do anything else that doesn't
355 involve knowing anything about the recipes in the
356 current configuration. False loads the cache / parses
357 recipes.
358 config_params: optionally specify your own configuration
359 parameters. If not specified an instance of
360 TinfoilConfigParameters will be created internally.
361 quiet: quiet level controlling console output - equivalent
362 to bitbake's -q/--quiet option. Default of 0 gives
363 the same output level as normal bitbake execution.
364 extra_features: extra features to be added to the feature
365 set requested from the server. See
366 CookerFeatures._feature_list for possible
367 features.
368 """
369 self.quiet = quiet
370
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500371 if self.tracking:
372 extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING]
373 else:
374 extrafeatures = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500375
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500376 if extra_features:
377 extrafeatures += extra_features
378
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500379 if not config_params:
380 config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500381
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500382 cookerconfig = CookerConfiguration()
383 cookerconfig.setConfigParameters(config_params)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500384
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500385 if not config_only:
386 # Disable local loggers because the UI module is going to set up its own
387 for handler in self.localhandlers:
388 self.logger.handlers.remove(handler)
389 self.localhandlers = []
390
391 self.server_connection, ui_module = setup_bitbake(config_params,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500392 cookerconfig,
393 extrafeatures)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500394
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500395 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)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500412 if config_only:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500413 config_params.updateToServer(self.server_connection.connection, os.environ.copy())
414 self.run_command('parseConfiguration')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500415 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500416 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500417 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500418
Andrew Geissler82c905d2020-04-13 13:39:40 -0500419 self.config_data = TinfoilDataStoreConnector(self, 0)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500420 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 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500435 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
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500442 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)
448 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500449 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500450
451 def run_command(self, command, *params):
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 result = self.server_connection.connection.runCommand(commandline)
469 if result[1]:
470 raise TinfoilCommandFailed(result[1])
471 return result[0]
472
473 def set_event_mask(self, eventlist):
474 """Set the event mask which will be applied within wait_event()"""
475 if not self.server_connection:
476 raise Exception('Not connected to server (did you call .prepare()?)')
477 llevel, debug_domains = bb.msg.constructLogOptions()
478 ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
479 if not ret:
480 raise Exception('setEventMask failed')
481
482 def wait_event(self, timeout=0):
483 """
484 Wait for an event from the server for the specified time.
485 A timeout of 0 means don't wait if there are no events in the queue.
486 Returns the next event in the queue or None if the timeout was
487 reached. Note that in order to recieve any events you will
488 first need to set the internal event mask using set_event_mask()
489 (otherwise whatever event mask the UI set up will be in effect).
490 """
491 if not self.server_connection:
492 raise Exception('Not connected to server (did you call .prepare()?)')
493 return self.server_connection.events.waitEvent(timeout)
494
495 def get_overlayed_recipes(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500496 """
497 Find recipes which are overlayed (i.e. where recipes exist in multiple layers)
498 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500499 return defaultdict(list, self.run_command('getOverlayedRecipes'))
500
501 def get_skipped_recipes(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500502 """
503 Find recipes which were skipped (i.e. SkipRecipe was raised
504 during parsing).
505 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500506 return OrderedDict(self.run_command('getSkippedRecipes'))
507
Andrew Geissler82c905d2020-04-13 13:39:40 -0500508 def get_all_providers(self, mc=''):
509 return defaultdict(list, self.run_command('allProviders', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500510
Andrew Geissler82c905d2020-04-13 13:39:40 -0500511 def find_providers(self, mc=''):
512 return self.run_command('findProviders', mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500513
514 def find_best_provider(self, pn):
515 return self.run_command('findBestProvider', pn)
516
517 def get_runtime_providers(self, rdep):
518 return self.run_command('getRuntimeProviders', rdep)
519
520 def get_recipe_file(self, pn):
521 """
522 Get the file name for the specified recipe/target. Raises
523 bb.providers.NoProvider if there is no match or the recipe was
524 skipped.
525 """
526 best = self.find_best_provider(pn)
527 if not best or (len(best) > 3 and not best[3]):
528 skiplist = self.get_skipped_recipes()
529 taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
530 skipreasons = taskdata.get_reasons(pn)
531 if skipreasons:
532 raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons)))
533 else:
534 raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
535 return best[3]
536
537 def get_file_appends(self, fn):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500538 """
539 Find the bbappends for a recipe file
540 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500541 return self.run_command('getFileAppends', fn)
542
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500543 def all_recipes(self, mc='', sort=True):
544 """
545 Enable iterating over all recipes in the current configuration.
546 Returns an iterator over TinfoilRecipeInfo objects created on demand.
547 Parameters:
548 mc: The multiconfig, default of '' uses the main configuration.
549 sort: True to sort recipes alphabetically (default), False otherwise
550 """
551 recipecache = self.cooker.recipecaches[mc]
552 if sort:
553 recipes = sorted(recipecache.pkg_pn.items())
554 else:
555 recipes = recipecache.pkg_pn.items()
556 for pn, fns in recipes:
557 prov = self.find_best_provider(pn)
558 recipe = TinfoilRecipeInfo(recipecache,
559 self.config_data,
560 pn=pn,
561 fn=prov[3],
562 fns=fns)
563 yield recipe
564
565 def all_recipe_files(self, mc='', variants=True, preferred_only=False):
566 """
567 Enable iterating over all recipe files in the current configuration.
568 Returns an iterator over file paths.
569 Parameters:
570 mc: The multiconfig, default of '' uses the main configuration.
571 variants: True to include variants of recipes created through
572 BBCLASSEXTEND (default) or False to exclude them
573 preferred_only: True to include only the preferred recipe where
574 multiple exist providing the same PN, False to list
575 all recipes
576 """
577 recipecache = self.cooker.recipecaches[mc]
578 if preferred_only:
579 files = []
580 for pn in recipecache.pkg_pn.keys():
581 prov = self.find_best_provider(pn)
582 files.append(prov[3])
583 else:
584 files = recipecache.pkg_fn.keys()
585 for fn in sorted(files):
586 if not variants and fn.startswith('virtual:'):
587 continue
588 yield fn
589
590
591 def get_recipe_info(self, pn, mc=''):
592 """
593 Get information on a specific recipe in the current configuration by name (PN).
594 Returns a TinfoilRecipeInfo object created on demand.
595 Parameters:
596 mc: The multiconfig, default of '' uses the main configuration.
597 """
598 recipecache = self.cooker.recipecaches[mc]
599 prov = self.find_best_provider(pn)
600 fn = prov[3]
Brad Bishop316dfdd2018-06-25 12:45:53 -0400601 if fn:
602 actual_pn = recipecache.pkg_fn[fn]
603 recipe = TinfoilRecipeInfo(recipecache,
604 self.config_data,
605 pn=actual_pn,
606 fn=fn,
607 fns=recipecache.pkg_pn[actual_pn])
608 return recipe
609 else:
610 return None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500611
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500612 def parse_recipe(self, pn):
613 """
614 Parse the specified recipe and return a datastore object
615 representing the environment for the recipe.
616 """
617 fn = self.get_recipe_file(pn)
618 return self.parse_recipe_file(fn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500619
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600620 def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
621 """
622 Parse the specified recipe file (with or without bbappends)
623 and return a datastore object representing the environment
624 for the recipe.
625 Parameters:
626 fn: recipe file to parse - can be a file path or virtual
627 specification
628 appends: True to apply bbappends, False otherwise
629 appendlist: optional list of bbappend files to apply, if you
630 want to filter them
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600631 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500632 if self.tracking:
633 # Enable history tracking just for the parse operation
634 self.run_command('enableDataTracking')
635 try:
636 if appends and appendlist == []:
637 appends = False
638 if config_data:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500639 config_data = bb.data.createCopy(config_data)
640 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500641 else:
642 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
643 if dscon:
644 return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
645 else:
646 return None
647 finally:
648 if self.tracking:
649 self.run_command('disableDataTracking')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500650
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500651 def build_file(self, buildfile, task, internal=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500652 """
653 Runs the specified task for just a single recipe (i.e. no dependencies).
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500654 This is equivalent to bitbake -b, except with the default internal=True
655 no warning about dependencies will be produced, normal info messages
656 from the runqueue will be silenced and BuildInit, BuildStarted and
657 BuildCompleted events will not be fired.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500658 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500659 return self.run_command('buildFile', buildfile, task, internal)
660
661 def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None):
662 """
663 Builds the specified targets. This is equivalent to a normal invocation
664 of bitbake. Has built-in event handling which is enabled by default and
665 can be extended if needed.
666 Parameters:
667 targets:
668 One or more targets to build. Can be a list or a
669 space-separated string.
670 task:
671 The task to run; if None then the value of BB_DEFAULT_TASK
672 will be used. Default None.
673 handle_events:
674 True to handle events in a similar way to normal bitbake
675 invocation with knotty; False to return immediately (on the
676 assumption that the caller will handle the events instead).
677 Default True.
678 extra_events:
679 An optional list of events to add to the event mask (if
680 handle_events=True). If you add events here you also need
681 to specify a callback function in event_callback that will
682 handle the additional events. Default None.
683 event_callback:
684 An optional function taking a single parameter which
685 will be called first upon receiving any event (if
686 handle_events=True) so that the caller can override or
687 extend the event handling. Default None.
688 """
689 if isinstance(targets, str):
690 targets = targets.split()
691 if not task:
692 task = self.config_data.getVar('BB_DEFAULT_TASK')
693
694 if handle_events:
695 # A reasonable set of default events matching up with those we handle below
696 eventmask = [
697 'bb.event.BuildStarted',
698 'bb.event.BuildCompleted',
699 'logging.LogRecord',
700 'bb.event.NoProvider',
701 'bb.command.CommandCompleted',
702 'bb.command.CommandFailed',
703 'bb.build.TaskStarted',
704 'bb.build.TaskFailed',
705 'bb.build.TaskSucceeded',
706 'bb.build.TaskFailedSilent',
707 'bb.build.TaskProgress',
708 'bb.runqueue.runQueueTaskStarted',
709 'bb.runqueue.sceneQueueTaskStarted',
710 'bb.event.ProcessStarted',
711 'bb.event.ProcessProgress',
712 'bb.event.ProcessFinished',
713 ]
714 if extra_events:
715 eventmask.extend(extra_events)
716 ret = self.set_event_mask(eventmask)
717
718 includelogs = self.config_data.getVar('BBINCLUDELOGS')
719 loglines = self.config_data.getVar('BBINCLUDELOGS_LINES')
720
721 ret = self.run_command('buildTargets', targets, task)
722 if handle_events:
723 result = False
724 # Borrowed from knotty, instead somewhat hackily we use the helper
725 # as the object to store "shutdown" on
726 helper = bb.ui.uihelper.BBUIHelper()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500727 helper.shutdown = 0
728 parseprogress = None
Andrew Geissler82c905d2020-04-13 13:39:40 -0500729 termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500730 try:
731 while True:
732 try:
733 event = self.wait_event(0.25)
734 if event:
735 if event_callback and event_callback(event):
736 continue
737 if helper.eventHandler(event):
738 if isinstance(event, bb.build.TaskFailedSilent):
739 logger.warning("Logfile for failed setscene task is %s" % event.logfile)
740 elif isinstance(event, bb.build.TaskFailed):
741 bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter)
742 continue
743 if isinstance(event, bb.event.ProcessStarted):
744 if self.quiet > 1:
745 continue
746 parseprogress = bb.ui.knotty.new_progress(event.processname, event.total)
747 parseprogress.start(False)
748 continue
749 if isinstance(event, bb.event.ProcessProgress):
750 if self.quiet > 1:
751 continue
752 if parseprogress:
753 parseprogress.update(event.progress)
754 else:
755 bb.warn("Got ProcessProgress event for someting that never started?")
756 continue
757 if isinstance(event, bb.event.ProcessFinished):
758 if self.quiet > 1:
759 continue
760 if parseprogress:
761 parseprogress.finish()
762 parseprogress = None
763 continue
764 if isinstance(event, bb.command.CommandCompleted):
765 result = True
766 break
767 if isinstance(event, bb.command.CommandFailed):
768 self.logger.error(str(event))
769 result = False
770 break
771 if isinstance(event, logging.LogRecord):
772 if event.taskpid == 0 or event.levelno > logging.INFO:
773 self.logger.handle(event)
774 continue
775 if isinstance(event, bb.event.NoProvider):
776 self.logger.error(str(event))
777 result = False
778 break
779
780 elif helper.shutdown > 1:
781 break
782 termfilter.updateFooter()
783 except KeyboardInterrupt:
784 termfilter.clearFooter()
785 if helper.shutdown == 1:
786 print("\nSecond Keyboard Interrupt, stopping...\n")
787 ret = self.run_command("stateForceShutdown")
788 if ret and ret[2]:
789 self.logger.error("Unable to cleanly stop: %s" % ret[2])
790 elif helper.shutdown == 0:
791 print("\nKeyboard Interrupt, closing down...\n")
792 interrupted = True
793 ret = self.run_command("stateShutdown")
794 if ret and ret[2]:
795 self.logger.error("Unable to cleanly shutdown: %s" % ret[2])
796 helper.shutdown = helper.shutdown + 1
797 termfilter.clearFooter()
798 finally:
799 termfilter.finish()
800 if helper.failed_tasks:
801 result = False
802 return result
803 else:
804 return ret
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600805
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500806 def shutdown(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500807 """
808 Shut down tinfoil. Disconnects from the server and gracefully
809 releases any associated resources. You must call this function if
810 prepare() has been called, or use a with... block when you create
811 the tinfoil object which will ensure that it gets called.
812 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500813 if self.server_connection:
814 self.run_command('clientComplete')
815 _server_connections.remove(self.server_connection)
816 bb.event.ui_queue = []
817 self.server_connection.terminate()
818 self.server_connection = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500819
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500820 # Restore logging handlers to how it looked when we started
821 if self.oldhandlers:
822 for handler in self.logger.handlers:
823 if handler not in self.oldhandlers:
824 self.logger.handlers.remove(handler)
825
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500826 def _reconvert_type(self, obj, origtypename):
827 """
828 Convert an object back to the right type, in the case
829 that marshalling has changed it (especially with xmlrpc)
830 """
831 supported_types = {
832 'set': set,
833 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
834 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500835
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500836 origtype = supported_types.get(origtypename, None)
837 if origtype is None:
838 raise Exception('Unsupported type "%s"' % origtypename)
839 if type(obj) == origtype:
840 newobj = obj
841 elif isinstance(obj, dict):
842 # New style class
843 newobj = origtype()
844 for k,v in obj.items():
845 setattr(newobj, k, v)
846 else:
847 # Assume we can coerce the type
848 newobj = origtype(obj)
849
850 if isinstance(newobj, bb.command.DataStoreConnectionHandle):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500851 newobj = TinfoilDataStoreConnector(self, newobj.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500852
853 return newobj
854
855
856class TinfoilConfigParameters(BitBakeConfigParameters):
857
858 def __init__(self, config_only, **options):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500859 self.initial_options = options
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500860 # Apply some sane defaults
861 if not 'parse_only' in options:
862 self.initial_options['parse_only'] = not config_only
863 #if not 'status_only' in options:
864 # self.initial_options['status_only'] = config_only
865 if not 'ui' in options:
866 self.initial_options['ui'] = 'knotty'
867 if not 'argv' in options:
868 self.initial_options['argv'] = []
869
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500870 super(TinfoilConfigParameters, self).__init__()
871
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500872 def parseCommandLine(self, argv=None):
873 # We don't want any parameters parsed from the command line
874 opts = super(TinfoilConfigParameters, self).parseCommandLine([])
875 for key, val in self.initial_options.items():
876 setattr(opts[0], key, val)
877 return opts