blob: dccbe0ebb5de552eeb3b4ca460fd721b556c70a4 [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 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500120 def __init__(self, tinfoil, mc=''):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500121 self.tinfoil = tinfoil
Andrew Geissler5a43b432020-06-13 10:46:56 -0500122 self.mc = mc
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500123 def get_file_appends(self, fn):
Andrew Geissler5a43b432020-06-13 10:46:56 -0500124 return self.tinfoil.get_file_appends(fn, self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500125 def __getattr__(self, name):
126 if name == 'overlayed':
Andrew Geissler5a43b432020-06-13 10:46:56 -0500127 return self.tinfoil.get_overlayed_recipes(self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500128 elif name == 'bbappends':
Andrew Geissler5a43b432020-06-13 10:46:56 -0500129 return self.tinfoil.run_command('getAllAppends', self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500130 else:
131 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
132
133 class TinfoilRecipeCacheAdapter:
134 """ cooker.recipecache adapter """
Andrew Geissler82c905d2020-04-13 13:39:40 -0500135 def __init__(self, tinfoil, mc=''):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500136 self.tinfoil = tinfoil
Andrew Geissler82c905d2020-04-13 13:39:40 -0500137 self.mc = mc
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500138 self._cache = {}
139
140 def get_pkg_pn_fn(self):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500141 pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500142 pkg_fn = {}
143 for pn, fnlist in pkg_pn.items():
144 for fn in fnlist:
145 pkg_fn[fn] = pn
146 self._cache['pkg_pn'] = pkg_pn
147 self._cache['pkg_fn'] = pkg_fn
148
149 def __getattr__(self, name):
150 # Grab these only when they are requested since they aren't always used
151 if name in self._cache:
152 return self._cache[name]
153 elif name == 'pkg_pn':
154 self.get_pkg_pn_fn()
155 return self._cache[name]
156 elif name == 'pkg_fn':
157 self.get_pkg_pn_fn()
158 return self._cache[name]
159 elif name == 'deps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500160 attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500161 elif name == 'rundeps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500162 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500163 elif name == 'runrecs':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500164 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500165 elif name == 'pkg_pepvpr':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500166 attrvalue = self.tinfoil.run_command('getRecipeVersions', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500167 elif name == 'inherits':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500168 attrvalue = self.tinfoil.run_command('getRecipeInherits', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500169 elif name == 'bbfile_priority':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500170 attrvalue = self.tinfoil.run_command('getBbFilePriority', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500171 elif name == 'pkg_dp':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500172 attrvalue = self.tinfoil.run_command('getDefaultPreference', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500173 elif name == 'fn_provides':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500174 attrvalue = self.tinfoil.run_command('getRecipeProvides', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500175 elif name == 'packages':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500176 attrvalue = self.tinfoil.run_command('getRecipePackages', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500177 elif name == 'packages_dynamic':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500178 attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500179 elif name == 'rproviders':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500180 attrvalue = self.tinfoil.run_command('getRProviders', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500181 else:
182 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
183
184 self._cache[name] = attrvalue
185 return attrvalue
186
187 def __init__(self, tinfoil):
188 self.tinfoil = tinfoil
Andrew Geissler5a43b432020-06-13 10:46:56 -0500189 self.multiconfigs = [''] + (tinfoil.config_data.getVar('BBMULTICONFIG') or '').split()
190 self.collections = {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500191 self.recipecaches = {}
Andrew Geissler5a43b432020-06-13 10:46:56 -0500192 for mc in self.multiconfigs:
193 self.collections[mc] = self.TinfoilCookerCollectionAdapter(tinfoil, mc)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500194 self.recipecaches[mc] = self.TinfoilRecipeCacheAdapter(tinfoil, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500195 self._cache = {}
196 def __getattr__(self, name):
197 # Grab these only when they are requested since they aren't always used
198 if name in self._cache:
199 return self._cache[name]
200 elif name == 'skiplist':
201 attrvalue = self.tinfoil.get_skipped_recipes()
202 elif name == 'bbfile_config_priorities':
203 ret = self.tinfoil.run_command('getLayerPriorities')
204 bbfile_config_priorities = []
205 for collection, pattern, regex, pri in ret:
206 bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri))
207
208 attrvalue = bbfile_config_priorities
209 else:
210 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
211
212 self._cache[name] = attrvalue
213 return attrvalue
214
215 def findBestProvider(self, pn):
216 return self.tinfoil.find_best_provider(pn)
217
218
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500219class TinfoilRecipeInfo:
220 """
221 Provides a convenient representation of the cached information for a single recipe.
222 Some attributes are set on construction, others are read on-demand (which internally
223 may result in a remote procedure call to the bitbake server the first time).
224 Note that only information which is cached is available through this object - if
225 you need other variable values you will need to parse the recipe using
226 Tinfoil.parse_recipe().
227 """
228 def __init__(self, recipecache, d, pn, fn, fns):
229 self._recipecache = recipecache
230 self._d = d
231 self.pn = pn
232 self.fn = fn
233 self.fns = fns
234 self.inherit_files = recipecache.inherits[fn]
235 self.depends = recipecache.deps[fn]
236 (self.pe, self.pv, self.pr) = recipecache.pkg_pepvpr[fn]
237 self._cached_packages = None
238 self._cached_rprovides = None
239 self._cached_packages_dynamic = None
240
241 def __getattr__(self, name):
242 if name == 'alternates':
243 return [x for x in self.fns if x != self.fn]
244 elif name == 'rdepends':
245 return self._recipecache.rundeps[self.fn]
246 elif name == 'rrecommends':
247 return self._recipecache.runrecs[self.fn]
248 elif name == 'provides':
249 return self._recipecache.fn_provides[self.fn]
250 elif name == 'packages':
251 if self._cached_packages is None:
252 self._cached_packages = []
253 for pkg, fns in self._recipecache.packages.items():
254 if self.fn in fns:
255 self._cached_packages.append(pkg)
256 return self._cached_packages
257 elif name == 'packages_dynamic':
258 if self._cached_packages_dynamic is None:
259 self._cached_packages_dynamic = []
260 for pkg, fns in self._recipecache.packages_dynamic.items():
261 if self.fn in fns:
262 self._cached_packages_dynamic.append(pkg)
263 return self._cached_packages_dynamic
264 elif name == 'rprovides':
265 if self._cached_rprovides is None:
266 self._cached_rprovides = []
267 for pkg, fns in self._recipecache.rproviders.items():
268 if self.fn in fns:
269 self._cached_rprovides.append(pkg)
270 return self._cached_rprovides
271 else:
272 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
273 def inherits(self, only_recipe=False):
274 """
275 Get the inherited classes for a recipe. Returns the class names only.
276 Parameters:
277 only_recipe: True to return only the classes inherited by the recipe
278 itself, False to return all classes inherited within
279 the context for the recipe (which includes globally
280 inherited classes).
281 """
282 if only_recipe:
283 global_inherit = [x for x in (self._d.getVar('BBINCLUDED') or '').split() if x.endswith('.bbclass')]
284 else:
285 global_inherit = []
286 for clsfile in self.inherit_files:
287 if only_recipe and clsfile in global_inherit:
288 continue
289 clsname = os.path.splitext(os.path.basename(clsfile))[0]
290 yield clsname
291 def __str__(self):
292 return '%s' % self.pn
293
294
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500295class Tinfoil:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500296 """
297 Tinfoil - an API for scripts and utilities to query
298 BitBake internals and perform build operations.
299 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500300
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500301 def __init__(self, output=sys.stdout, tracking=False, setup_logging=True):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500302 """
303 Create a new tinfoil object.
304 Parameters:
305 output: specifies where console output should be sent. Defaults
306 to sys.stdout.
307 tracking: True to enable variable history tracking, False to
308 disable it (default). Enabling this has a minor
309 performance impact so typically it isn't enabled
310 unless you need to query variable history.
311 setup_logging: True to setup a logger so that things like
312 bb.warn() will work immediately and timeout warnings
313 are visible; False to let BitBake do this itself.
314 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500315 self.logger = logging.getLogger('BitBake')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500316 self.config_data = None
317 self.cooker = None
318 self.tracking = tracking
319 self.ui_module = None
320 self.server_connection = None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500321 self.recipes_parsed = False
322 self.quiet = 0
323 self.oldhandlers = self.logger.handlers[:]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500324 if setup_logging:
325 # This is the *client-side* logger, nothing to do with
326 # logging messages from the server
327 bb.msg.logger_create('BitBake', output)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500328 self.localhandlers = []
329 for handler in self.logger.handlers:
330 if handler not in self.oldhandlers:
331 self.localhandlers.append(handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500332
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600333 def __enter__(self):
334 return self
335
336 def __exit__(self, type, value, traceback):
337 self.shutdown()
338
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500339 def prepare(self, config_only=False, config_params=None, quiet=0, extra_features=None):
340 """
341 Prepares the underlying BitBake system to be used via tinfoil.
342 This function must be called prior to calling any of the other
343 functions in the API.
344 NOTE: if you call prepare() you must absolutely call shutdown()
345 before your code terminates. You can use a "with" block to ensure
346 this happens e.g.
347
348 with bb.tinfoil.Tinfoil() as tinfoil:
349 tinfoil.prepare()
350 ...
351
352 Parameters:
353 config_only: True to read only the configuration and not load
354 the cache / parse recipes. This is useful if you just
355 want to query the value of a variable at the global
356 level or you want to do anything else that doesn't
357 involve knowing anything about the recipes in the
358 current configuration. False loads the cache / parses
359 recipes.
360 config_params: optionally specify your own configuration
361 parameters. If not specified an instance of
362 TinfoilConfigParameters will be created internally.
363 quiet: quiet level controlling console output - equivalent
364 to bitbake's -q/--quiet option. Default of 0 gives
365 the same output level as normal bitbake execution.
366 extra_features: extra features to be added to the feature
367 set requested from the server. See
368 CookerFeatures._feature_list for possible
369 features.
370 """
371 self.quiet = quiet
372
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500373 if self.tracking:
374 extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING]
375 else:
376 extrafeatures = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500377
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500378 if extra_features:
379 extrafeatures += extra_features
380
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500381 if not config_params:
382 config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500383
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500384 cookerconfig = CookerConfiguration()
385 cookerconfig.setConfigParameters(config_params)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500386
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500387 if not config_only:
388 # Disable local loggers because the UI module is going to set up its own
389 for handler in self.localhandlers:
390 self.logger.handlers.remove(handler)
391 self.localhandlers = []
392
393 self.server_connection, ui_module = setup_bitbake(config_params,
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500394 cookerconfig,
395 extrafeatures)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500396
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500397 self.ui_module = ui_module
398
399 # Ensure the path to bitbake's bin directory is in PATH so that things like
400 # bitbake-worker can be run (usually this is the case, but it doesn't have to be)
401 path = os.getenv('PATH').split(':')
402 bitbakebinpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'bin'))
403 for entry in path:
404 if entry.endswith(os.sep):
405 entry = entry[:-1]
406 if os.path.abspath(entry) == bitbakebinpath:
407 break
408 else:
409 path.insert(0, bitbakebinpath)
410 os.environ['PATH'] = ':'.join(path)
411
412 if self.server_connection:
413 _server_connections.append(self.server_connection)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500414 if config_only:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500415 config_params.updateToServer(self.server_connection.connection, os.environ.copy())
416 self.run_command('parseConfiguration')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500417 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500418 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500419 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500420
Andrew Geissler82c905d2020-04-13 13:39:40 -0500421 self.config_data = TinfoilDataStoreConnector(self, 0)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500422 self.cooker = TinfoilCookerAdapter(self)
423 self.cooker_data = self.cooker.recipecaches['']
424 else:
425 raise Exception('Failed to start bitbake server')
426
427 def run_actions(self, config_params):
428 """
429 Run the actions specified in config_params through the UI.
430 """
431 ret = self.ui_module.main(self.server_connection.connection, self.server_connection.events, config_params)
432 if ret:
433 raise TinfoilUIException(ret)
434
435 def parseRecipes(self):
436 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500437 Legacy function - use parse_recipes() instead.
438 """
439 self.parse_recipes()
440
441 def parse_recipes(self):
442 """
443 Load information on all recipes. Normally you should specify
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500444 config_only=False when calling prepare() instead of using this
445 function; this function is designed for situations where you need
446 to initialise Tinfoil and use it with config_only=True first and
447 then conditionally call this function to parse recipes later.
448 """
449 config_params = TinfoilConfigParameters(config_only=False)
450 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500451 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500452
453 def run_command(self, command, *params):
454 """
455 Run a command on the server (as implemented in bb.command).
456 Note that there are two types of command - synchronous and
457 asynchronous; in order to receive the results of asynchronous
458 commands you will need to set an appropriate event mask
459 using set_event_mask() and listen for the result using
460 wait_event() - with the correct event mask you'll at least get
461 bb.command.CommandCompleted and possibly other events before
462 that depending on the command.
463 """
464 if not self.server_connection:
465 raise Exception('Not connected to server (did you call .prepare()?)')
466
467 commandline = [command]
468 if params:
469 commandline.extend(params)
470 result = self.server_connection.connection.runCommand(commandline)
471 if result[1]:
472 raise TinfoilCommandFailed(result[1])
473 return result[0]
474
475 def set_event_mask(self, eventlist):
476 """Set the event mask which will be applied within wait_event()"""
477 if not self.server_connection:
478 raise Exception('Not connected to server (did you call .prepare()?)')
479 llevel, debug_domains = bb.msg.constructLogOptions()
480 ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
481 if not ret:
482 raise Exception('setEventMask failed')
483
484 def wait_event(self, timeout=0):
485 """
486 Wait for an event from the server for the specified time.
487 A timeout of 0 means don't wait if there are no events in the queue.
488 Returns the next event in the queue or None if the timeout was
489 reached. Note that in order to recieve any events you will
490 first need to set the internal event mask using set_event_mask()
491 (otherwise whatever event mask the UI set up will be in effect).
492 """
493 if not self.server_connection:
494 raise Exception('Not connected to server (did you call .prepare()?)')
495 return self.server_connection.events.waitEvent(timeout)
496
Andrew Geissler5a43b432020-06-13 10:46:56 -0500497 def get_overlayed_recipes(self, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500498 """
499 Find recipes which are overlayed (i.e. where recipes exist in multiple layers)
500 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500501 return defaultdict(list, self.run_command('getOverlayedRecipes', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500502
503 def get_skipped_recipes(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500504 """
505 Find recipes which were skipped (i.e. SkipRecipe was raised
506 during parsing).
507 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500508 return OrderedDict(self.run_command('getSkippedRecipes'))
509
Andrew Geissler82c905d2020-04-13 13:39:40 -0500510 def get_all_providers(self, mc=''):
511 return defaultdict(list, self.run_command('allProviders', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500512
Andrew Geissler82c905d2020-04-13 13:39:40 -0500513 def find_providers(self, mc=''):
514 return self.run_command('findProviders', mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500515
516 def find_best_provider(self, pn):
517 return self.run_command('findBestProvider', pn)
518
519 def get_runtime_providers(self, rdep):
520 return self.run_command('getRuntimeProviders', rdep)
521
522 def get_recipe_file(self, pn):
523 """
524 Get the file name for the specified recipe/target. Raises
525 bb.providers.NoProvider if there is no match or the recipe was
526 skipped.
527 """
528 best = self.find_best_provider(pn)
529 if not best or (len(best) > 3 and not best[3]):
530 skiplist = self.get_skipped_recipes()
531 taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
532 skipreasons = taskdata.get_reasons(pn)
533 if skipreasons:
534 raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons)))
535 else:
536 raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
537 return best[3]
538
Andrew Geissler5a43b432020-06-13 10:46:56 -0500539 def get_file_appends(self, fn, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500540 """
541 Find the bbappends for a recipe file
542 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500543 return self.run_command('getFileAppends', fn, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500544
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500545 def all_recipes(self, mc='', sort=True):
546 """
547 Enable iterating over all recipes in the current configuration.
548 Returns an iterator over TinfoilRecipeInfo objects created on demand.
549 Parameters:
550 mc: The multiconfig, default of '' uses the main configuration.
551 sort: True to sort recipes alphabetically (default), False otherwise
552 """
553 recipecache = self.cooker.recipecaches[mc]
554 if sort:
555 recipes = sorted(recipecache.pkg_pn.items())
556 else:
557 recipes = recipecache.pkg_pn.items()
558 for pn, fns in recipes:
559 prov = self.find_best_provider(pn)
560 recipe = TinfoilRecipeInfo(recipecache,
561 self.config_data,
562 pn=pn,
563 fn=prov[3],
564 fns=fns)
565 yield recipe
566
567 def all_recipe_files(self, mc='', variants=True, preferred_only=False):
568 """
569 Enable iterating over all recipe files in the current configuration.
570 Returns an iterator over file paths.
571 Parameters:
572 mc: The multiconfig, default of '' uses the main configuration.
573 variants: True to include variants of recipes created through
574 BBCLASSEXTEND (default) or False to exclude them
575 preferred_only: True to include only the preferred recipe where
576 multiple exist providing the same PN, False to list
577 all recipes
578 """
579 recipecache = self.cooker.recipecaches[mc]
580 if preferred_only:
581 files = []
582 for pn in recipecache.pkg_pn.keys():
583 prov = self.find_best_provider(pn)
584 files.append(prov[3])
585 else:
586 files = recipecache.pkg_fn.keys()
587 for fn in sorted(files):
588 if not variants and fn.startswith('virtual:'):
589 continue
590 yield fn
591
592
593 def get_recipe_info(self, pn, mc=''):
594 """
595 Get information on a specific recipe in the current configuration by name (PN).
596 Returns a TinfoilRecipeInfo object created on demand.
597 Parameters:
598 mc: The multiconfig, default of '' uses the main configuration.
599 """
600 recipecache = self.cooker.recipecaches[mc]
601 prov = self.find_best_provider(pn)
602 fn = prov[3]
Brad Bishop316dfdd2018-06-25 12:45:53 -0400603 if fn:
604 actual_pn = recipecache.pkg_fn[fn]
605 recipe = TinfoilRecipeInfo(recipecache,
606 self.config_data,
607 pn=actual_pn,
608 fn=fn,
609 fns=recipecache.pkg_pn[actual_pn])
610 return recipe
611 else:
612 return None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500613
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500614 def parse_recipe(self, pn):
615 """
616 Parse the specified recipe and return a datastore object
617 representing the environment for the recipe.
618 """
619 fn = self.get_recipe_file(pn)
620 return self.parse_recipe_file(fn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500621
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600622 def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
623 """
624 Parse the specified recipe file (with or without bbappends)
625 and return a datastore object representing the environment
626 for the recipe.
627 Parameters:
628 fn: recipe file to parse - can be a file path or virtual
629 specification
630 appends: True to apply bbappends, False otherwise
631 appendlist: optional list of bbappend files to apply, if you
632 want to filter them
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600633 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500634 if self.tracking:
635 # Enable history tracking just for the parse operation
636 self.run_command('enableDataTracking')
637 try:
638 if appends and appendlist == []:
639 appends = False
640 if config_data:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500641 config_data = bb.data.createCopy(config_data)
642 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500643 else:
644 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
645 if dscon:
646 return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
647 else:
648 return None
649 finally:
650 if self.tracking:
651 self.run_command('disableDataTracking')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500652
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500653 def build_file(self, buildfile, task, internal=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500654 """
655 Runs the specified task for just a single recipe (i.e. no dependencies).
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500656 This is equivalent to bitbake -b, except with the default internal=True
657 no warning about dependencies will be produced, normal info messages
658 from the runqueue will be silenced and BuildInit, BuildStarted and
659 BuildCompleted events will not be fired.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500660 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500661 return self.run_command('buildFile', buildfile, task, internal)
662
663 def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None):
664 """
665 Builds the specified targets. This is equivalent to a normal invocation
666 of bitbake. Has built-in event handling which is enabled by default and
667 can be extended if needed.
668 Parameters:
669 targets:
670 One or more targets to build. Can be a list or a
671 space-separated string.
672 task:
673 The task to run; if None then the value of BB_DEFAULT_TASK
674 will be used. Default None.
675 handle_events:
676 True to handle events in a similar way to normal bitbake
677 invocation with knotty; False to return immediately (on the
678 assumption that the caller will handle the events instead).
679 Default True.
680 extra_events:
681 An optional list of events to add to the event mask (if
682 handle_events=True). If you add events here you also need
683 to specify a callback function in event_callback that will
684 handle the additional events. Default None.
685 event_callback:
686 An optional function taking a single parameter which
687 will be called first upon receiving any event (if
688 handle_events=True) so that the caller can override or
689 extend the event handling. Default None.
690 """
691 if isinstance(targets, str):
692 targets = targets.split()
693 if not task:
694 task = self.config_data.getVar('BB_DEFAULT_TASK')
695
696 if handle_events:
697 # A reasonable set of default events matching up with those we handle below
698 eventmask = [
699 'bb.event.BuildStarted',
700 'bb.event.BuildCompleted',
701 'logging.LogRecord',
702 'bb.event.NoProvider',
703 'bb.command.CommandCompleted',
704 'bb.command.CommandFailed',
705 'bb.build.TaskStarted',
706 'bb.build.TaskFailed',
707 'bb.build.TaskSucceeded',
708 'bb.build.TaskFailedSilent',
709 'bb.build.TaskProgress',
710 'bb.runqueue.runQueueTaskStarted',
711 'bb.runqueue.sceneQueueTaskStarted',
712 'bb.event.ProcessStarted',
713 'bb.event.ProcessProgress',
714 'bb.event.ProcessFinished',
715 ]
716 if extra_events:
717 eventmask.extend(extra_events)
718 ret = self.set_event_mask(eventmask)
719
720 includelogs = self.config_data.getVar('BBINCLUDELOGS')
721 loglines = self.config_data.getVar('BBINCLUDELOGS_LINES')
722
723 ret = self.run_command('buildTargets', targets, task)
724 if handle_events:
725 result = False
726 # Borrowed from knotty, instead somewhat hackily we use the helper
727 # as the object to store "shutdown" on
728 helper = bb.ui.uihelper.BBUIHelper()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500729 helper.shutdown = 0
730 parseprogress = None
Andrew Geissler82c905d2020-04-13 13:39:40 -0500731 termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500732 try:
733 while True:
734 try:
735 event = self.wait_event(0.25)
736 if event:
737 if event_callback and event_callback(event):
738 continue
739 if helper.eventHandler(event):
740 if isinstance(event, bb.build.TaskFailedSilent):
741 logger.warning("Logfile for failed setscene task is %s" % event.logfile)
742 elif isinstance(event, bb.build.TaskFailed):
743 bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter)
744 continue
745 if isinstance(event, bb.event.ProcessStarted):
746 if self.quiet > 1:
747 continue
748 parseprogress = bb.ui.knotty.new_progress(event.processname, event.total)
749 parseprogress.start(False)
750 continue
751 if isinstance(event, bb.event.ProcessProgress):
752 if self.quiet > 1:
753 continue
754 if parseprogress:
755 parseprogress.update(event.progress)
756 else:
757 bb.warn("Got ProcessProgress event for someting that never started?")
758 continue
759 if isinstance(event, bb.event.ProcessFinished):
760 if self.quiet > 1:
761 continue
762 if parseprogress:
763 parseprogress.finish()
764 parseprogress = None
765 continue
766 if isinstance(event, bb.command.CommandCompleted):
767 result = True
768 break
769 if isinstance(event, bb.command.CommandFailed):
770 self.logger.error(str(event))
771 result = False
772 break
773 if isinstance(event, logging.LogRecord):
774 if event.taskpid == 0 or event.levelno > logging.INFO:
775 self.logger.handle(event)
776 continue
777 if isinstance(event, bb.event.NoProvider):
778 self.logger.error(str(event))
779 result = False
780 break
781
782 elif helper.shutdown > 1:
783 break
784 termfilter.updateFooter()
785 except KeyboardInterrupt:
786 termfilter.clearFooter()
787 if helper.shutdown == 1:
788 print("\nSecond Keyboard Interrupt, stopping...\n")
789 ret = self.run_command("stateForceShutdown")
790 if ret and ret[2]:
791 self.logger.error("Unable to cleanly stop: %s" % ret[2])
792 elif helper.shutdown == 0:
793 print("\nKeyboard Interrupt, closing down...\n")
794 interrupted = True
795 ret = self.run_command("stateShutdown")
796 if ret and ret[2]:
797 self.logger.error("Unable to cleanly shutdown: %s" % ret[2])
798 helper.shutdown = helper.shutdown + 1
799 termfilter.clearFooter()
800 finally:
801 termfilter.finish()
802 if helper.failed_tasks:
803 result = False
804 return result
805 else:
806 return ret
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600807
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500808 def shutdown(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500809 """
810 Shut down tinfoil. Disconnects from the server and gracefully
811 releases any associated resources. You must call this function if
812 prepare() has been called, or use a with... block when you create
813 the tinfoil object which will ensure that it gets called.
814 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500815 if self.server_connection:
816 self.run_command('clientComplete')
817 _server_connections.remove(self.server_connection)
818 bb.event.ui_queue = []
819 self.server_connection.terminate()
820 self.server_connection = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500821
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500822 # Restore logging handlers to how it looked when we started
823 if self.oldhandlers:
824 for handler in self.logger.handlers:
825 if handler not in self.oldhandlers:
826 self.logger.handlers.remove(handler)
827
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500828 def _reconvert_type(self, obj, origtypename):
829 """
830 Convert an object back to the right type, in the case
831 that marshalling has changed it (especially with xmlrpc)
832 """
833 supported_types = {
834 'set': set,
835 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
836 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500837
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500838 origtype = supported_types.get(origtypename, None)
839 if origtype is None:
840 raise Exception('Unsupported type "%s"' % origtypename)
841 if type(obj) == origtype:
842 newobj = obj
843 elif isinstance(obj, dict):
844 # New style class
845 newobj = origtype()
846 for k,v in obj.items():
847 setattr(newobj, k, v)
848 else:
849 # Assume we can coerce the type
850 newobj = origtype(obj)
851
852 if isinstance(newobj, bb.command.DataStoreConnectionHandle):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500853 newobj = TinfoilDataStoreConnector(self, newobj.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500854
855 return newobj
856
857
858class TinfoilConfigParameters(BitBakeConfigParameters):
859
860 def __init__(self, config_only, **options):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500861 self.initial_options = options
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500862 # Apply some sane defaults
863 if not 'parse_only' in options:
864 self.initial_options['parse_only'] = not config_only
865 #if not 'status_only' in options:
866 # self.initial_options['status_only'] = config_only
867 if not 'ui' in options:
868 self.initial_options['ui'] = 'knotty'
869 if not 'argv' in options:
870 self.initial_options['argv'] = []
871
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500872 super(TinfoilConfigParameters, self).__init__()
873
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500874 def parseCommandLine(self, argv=None):
875 # We don't want any parameters parsed from the command line
876 opts = super(TinfoilConfigParameters, self).parseCommandLine([])
877 for key, val in self.initial_options.items():
878 setattr(opts[0], key, val)
879 return opts