blob: 91fbf1b13e5795ccffd4e4ce0cf465a1f43e0094 [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
Andrew Geissler517393d2023-01-13 08:55:19 -060013import time
Brad Bishop6e60e8b2018-02-01 10:27:11 -050014import atexit
15import re
16from collections import OrderedDict, defaultdict
Andrew Geissler82c905d2020-04-13 13:39:40 -050017from functools import partial
Patrick Williamsc124f4f2015-09-15 14:41:29 -050018
19import bb.cache
20import bb.cooker
21import bb.providers
Brad Bishop6e60e8b2018-02-01 10:27:11 -050022import bb.taskdata
Patrick Williamsc124f4f2015-09-15 14:41:29 -050023import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -050024import bb.command
25import bb.remotedata
Andrew Geissler82c905d2020-04-13 13:39:40 -050026from 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
Andrew Geisslerc926e172021-05-07 16:11:35 -050056 def emit(self, var, oval, val, o, d):
57 ret = self.tinfoil.run_command('dataStoreConnectorVarHistCmdEmit', self.dsindex, var, oval, val, d.dsindex)
58 o.write(ret)
59
Andrew Geissler82c905d2020-04-13 13:39:40 -050060 def __getattr__(self, name):
61 if not hasattr(bb.data_smart.VariableHistory, name):
62 raise AttributeError("VariableHistory has no such method %s" % name)
63
64 newfunc = partial(self.remoteCommand, name)
65 setattr(self, name, newfunc)
66 return newfunc
67
68class TinfoilDataStoreConnectorIncHistory:
69 def __init__(self, tinfoil, dsindex):
70 self.tinfoil = tinfoil
71 self.dsindex = dsindex
72
73 def remoteCommand(self, cmd, *args, **kwargs):
74 return self.tinfoil.run_command('dataStoreConnectorIncHistCmd', self.dsindex, cmd, args, kwargs)
75
76 def __getattr__(self, name):
77 if not hasattr(bb.data_smart.IncludeHistory, name):
78 raise AttributeError("IncludeHistory has no such method %s" % name)
79
80 newfunc = partial(self.remoteCommand, name)
81 setattr(self, name, newfunc)
82 return newfunc
83
Brad Bishop6e60e8b2018-02-01 10:27:11 -050084class TinfoilDataStoreConnector:
Andrew Geissler82c905d2020-04-13 13:39:40 -050085 """
86 Connector object used to enable access to datastore objects via tinfoil
87 Method calls are transmitted to the remote datastore for processing, if a datastore is
88 returned we return a connector object for the new store
89 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -050090
91 def __init__(self, tinfoil, dsindex):
92 self.tinfoil = tinfoil
93 self.dsindex = dsindex
Andrew Geissler82c905d2020-04-13 13:39:40 -050094 self.varhistory = TinfoilDataStoreConnectorVarHistory(tinfoil, dsindex)
95 self.inchistory = TinfoilDataStoreConnectorIncHistory(tinfoil, dsindex)
96
97 def remoteCommand(self, cmd, *args, **kwargs):
98 ret = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, cmd, args, kwargs)
99 if isinstance(ret, bb.command.DataStoreConnectionHandle):
100 return TinfoilDataStoreConnector(self.tinfoil, ret.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500101 return ret
Andrew Geissler82c905d2020-04-13 13:39:40 -0500102
103 def __getattr__(self, name):
104 if not hasattr(bb.data._dict_type, name):
105 raise AttributeError("Data store has no such method %s" % name)
106
107 newfunc = partial(self.remoteCommand, name)
108 setattr(self, name, newfunc)
109 return newfunc
110
111 def __iter__(self):
112 keys = self.tinfoil.run_command('dataStoreConnectorCmd', self.dsindex, "keys", [], {})
113 for k in keys:
114 yield k
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500115
116class TinfoilCookerAdapter:
117 """
118 Provide an adapter for existing code that expects to access a cooker object via Tinfoil,
119 since now Tinfoil is on the client side it no longer has direct access.
120 """
121
122 class TinfoilCookerCollectionAdapter:
123 """ cooker.collection adapter """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500124 def __init__(self, tinfoil, mc=''):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500125 self.tinfoil = tinfoil
Andrew Geissler5a43b432020-06-13 10:46:56 -0500126 self.mc = mc
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500127 def get_file_appends(self, fn):
Andrew Geissler5a43b432020-06-13 10:46:56 -0500128 return self.tinfoil.get_file_appends(fn, self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500129 def __getattr__(self, name):
130 if name == 'overlayed':
Andrew Geissler5a43b432020-06-13 10:46:56 -0500131 return self.tinfoil.get_overlayed_recipes(self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500132 elif name == 'bbappends':
Andrew Geissler5a43b432020-06-13 10:46:56 -0500133 return self.tinfoil.run_command('getAllAppends', self.mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500134 else:
135 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
136
137 class TinfoilRecipeCacheAdapter:
138 """ cooker.recipecache adapter """
Andrew Geissler82c905d2020-04-13 13:39:40 -0500139 def __init__(self, tinfoil, mc=''):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500140 self.tinfoil = tinfoil
Andrew Geissler82c905d2020-04-13 13:39:40 -0500141 self.mc = mc
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500142 self._cache = {}
143
144 def get_pkg_pn_fn(self):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500145 pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500146 pkg_fn = {}
147 for pn, fnlist in pkg_pn.items():
148 for fn in fnlist:
149 pkg_fn[fn] = pn
150 self._cache['pkg_pn'] = pkg_pn
151 self._cache['pkg_fn'] = pkg_fn
152
153 def __getattr__(self, name):
154 # Grab these only when they are requested since they aren't always used
155 if name in self._cache:
156 return self._cache[name]
157 elif name == 'pkg_pn':
158 self.get_pkg_pn_fn()
159 return self._cache[name]
160 elif name == 'pkg_fn':
161 self.get_pkg_pn_fn()
162 return self._cache[name]
163 elif name == 'deps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500164 attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500165 elif name == 'rundeps':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500166 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500167 elif name == 'runrecs':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500168 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends', self.mc) or [])
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500169 elif name == 'pkg_pepvpr':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500170 attrvalue = self.tinfoil.run_command('getRecipeVersions', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500171 elif name == 'inherits':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500172 attrvalue = self.tinfoil.run_command('getRecipeInherits', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500173 elif name == 'bbfile_priority':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500174 attrvalue = self.tinfoil.run_command('getBbFilePriority', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500175 elif name == 'pkg_dp':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500176 attrvalue = self.tinfoil.run_command('getDefaultPreference', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500177 elif name == 'fn_provides':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500178 attrvalue = self.tinfoil.run_command('getRecipeProvides', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500179 elif name == 'packages':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500180 attrvalue = self.tinfoil.run_command('getRecipePackages', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500181 elif name == 'packages_dynamic':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500182 attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic', self.mc) or {}
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500183 elif name == 'rproviders':
Andrew Geissler82c905d2020-04-13 13:39:40 -0500184 attrvalue = self.tinfoil.run_command('getRProviders', self.mc) or {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500185 else:
186 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
187
188 self._cache[name] = attrvalue
189 return attrvalue
190
191 def __init__(self, tinfoil):
192 self.tinfoil = tinfoil
Andrew Geissler5a43b432020-06-13 10:46:56 -0500193 self.multiconfigs = [''] + (tinfoil.config_data.getVar('BBMULTICONFIG') or '').split()
194 self.collections = {}
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500195 self.recipecaches = {}
Andrew Geissler5a43b432020-06-13 10:46:56 -0500196 for mc in self.multiconfigs:
197 self.collections[mc] = self.TinfoilCookerCollectionAdapter(tinfoil, mc)
Andrew Geissler82c905d2020-04-13 13:39:40 -0500198 self.recipecaches[mc] = self.TinfoilRecipeCacheAdapter(tinfoil, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500199 self._cache = {}
200 def __getattr__(self, name):
201 # Grab these only when they are requested since they aren't always used
202 if name in self._cache:
203 return self._cache[name]
204 elif name == 'skiplist':
205 attrvalue = self.tinfoil.get_skipped_recipes()
206 elif name == 'bbfile_config_priorities':
207 ret = self.tinfoil.run_command('getLayerPriorities')
208 bbfile_config_priorities = []
209 for collection, pattern, regex, pri in ret:
210 bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri))
211
212 attrvalue = bbfile_config_priorities
213 else:
214 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
215
216 self._cache[name] = attrvalue
217 return attrvalue
218
219 def findBestProvider(self, pn):
220 return self.tinfoil.find_best_provider(pn)
221
222
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500223class TinfoilRecipeInfo:
224 """
225 Provides a convenient representation of the cached information for a single recipe.
226 Some attributes are set on construction, others are read on-demand (which internally
227 may result in a remote procedure call to the bitbake server the first time).
228 Note that only information which is cached is available through this object - if
229 you need other variable values you will need to parse the recipe using
230 Tinfoil.parse_recipe().
231 """
232 def __init__(self, recipecache, d, pn, fn, fns):
233 self._recipecache = recipecache
234 self._d = d
235 self.pn = pn
236 self.fn = fn
237 self.fns = fns
238 self.inherit_files = recipecache.inherits[fn]
239 self.depends = recipecache.deps[fn]
240 (self.pe, self.pv, self.pr) = recipecache.pkg_pepvpr[fn]
241 self._cached_packages = None
242 self._cached_rprovides = None
243 self._cached_packages_dynamic = None
244
245 def __getattr__(self, name):
246 if name == 'alternates':
247 return [x for x in self.fns if x != self.fn]
248 elif name == 'rdepends':
249 return self._recipecache.rundeps[self.fn]
250 elif name == 'rrecommends':
251 return self._recipecache.runrecs[self.fn]
252 elif name == 'provides':
253 return self._recipecache.fn_provides[self.fn]
254 elif name == 'packages':
255 if self._cached_packages is None:
256 self._cached_packages = []
257 for pkg, fns in self._recipecache.packages.items():
258 if self.fn in fns:
259 self._cached_packages.append(pkg)
260 return self._cached_packages
261 elif name == 'packages_dynamic':
262 if self._cached_packages_dynamic is None:
263 self._cached_packages_dynamic = []
264 for pkg, fns in self._recipecache.packages_dynamic.items():
265 if self.fn in fns:
266 self._cached_packages_dynamic.append(pkg)
267 return self._cached_packages_dynamic
268 elif name == 'rprovides':
269 if self._cached_rprovides is None:
270 self._cached_rprovides = []
271 for pkg, fns in self._recipecache.rproviders.items():
272 if self.fn in fns:
273 self._cached_rprovides.append(pkg)
274 return self._cached_rprovides
275 else:
276 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
277 def inherits(self, only_recipe=False):
278 """
279 Get the inherited classes for a recipe. Returns the class names only.
280 Parameters:
281 only_recipe: True to return only the classes inherited by the recipe
282 itself, False to return all classes inherited within
283 the context for the recipe (which includes globally
284 inherited classes).
285 """
286 if only_recipe:
287 global_inherit = [x for x in (self._d.getVar('BBINCLUDED') or '').split() if x.endswith('.bbclass')]
288 else:
289 global_inherit = []
290 for clsfile in self.inherit_files:
291 if only_recipe and clsfile in global_inherit:
292 continue
293 clsname = os.path.splitext(os.path.basename(clsfile))[0]
294 yield clsname
295 def __str__(self):
296 return '%s' % self.pn
297
298
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500299class Tinfoil:
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500300 """
301 Tinfoil - an API for scripts and utilities to query
302 BitBake internals and perform build operations.
303 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500304
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500305 def __init__(self, output=sys.stdout, tracking=False, setup_logging=True):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500306 """
307 Create a new tinfoil object.
308 Parameters:
309 output: specifies where console output should be sent. Defaults
310 to sys.stdout.
311 tracking: True to enable variable history tracking, False to
312 disable it (default). Enabling this has a minor
313 performance impact so typically it isn't enabled
314 unless you need to query variable history.
315 setup_logging: True to setup a logger so that things like
316 bb.warn() will work immediately and timeout warnings
317 are visible; False to let BitBake do this itself.
318 """
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500319 self.logger = logging.getLogger('BitBake')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500320 self.config_data = None
321 self.cooker = None
322 self.tracking = tracking
323 self.ui_module = None
324 self.server_connection = None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500325 self.recipes_parsed = False
326 self.quiet = 0
327 self.oldhandlers = self.logger.handlers[:]
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500328 if setup_logging:
329 # This is the *client-side* logger, nothing to do with
330 # logging messages from the server
331 bb.msg.logger_create('BitBake', output)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500332 self.localhandlers = []
333 for handler in self.logger.handlers:
334 if handler not in self.oldhandlers:
335 self.localhandlers.append(handler)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500336
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600337 def __enter__(self):
338 return self
339
340 def __exit__(self, type, value, traceback):
341 self.shutdown()
342
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500343 def prepare(self, config_only=False, config_params=None, quiet=0, extra_features=None):
344 """
345 Prepares the underlying BitBake system to be used via tinfoil.
346 This function must be called prior to calling any of the other
347 functions in the API.
348 NOTE: if you call prepare() you must absolutely call shutdown()
349 before your code terminates. You can use a "with" block to ensure
350 this happens e.g.
351
352 with bb.tinfoil.Tinfoil() as tinfoil:
353 tinfoil.prepare()
354 ...
355
356 Parameters:
357 config_only: True to read only the configuration and not load
358 the cache / parse recipes. This is useful if you just
359 want to query the value of a variable at the global
360 level or you want to do anything else that doesn't
361 involve knowing anything about the recipes in the
362 current configuration. False loads the cache / parses
363 recipes.
364 config_params: optionally specify your own configuration
365 parameters. If not specified an instance of
366 TinfoilConfigParameters will be created internally.
367 quiet: quiet level controlling console output - equivalent
368 to bitbake's -q/--quiet option. Default of 0 gives
369 the same output level as normal bitbake execution.
370 extra_features: extra features to be added to the feature
371 set requested from the server. See
372 CookerFeatures._feature_list for possible
373 features.
374 """
375 self.quiet = quiet
376
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500377 if self.tracking:
378 extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING]
379 else:
380 extrafeatures = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500381
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500382 if extra_features:
383 extrafeatures += extra_features
384
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500385 if not config_params:
386 config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500387
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500388 if not config_only:
389 # Disable local loggers because the UI module is going to set up its own
390 for handler in self.localhandlers:
391 self.logger.handlers.remove(handler)
392 self.localhandlers = []
393
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500394 self.server_connection, ui_module = setup_bitbake(config_params, extrafeatures)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500395
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500396 self.ui_module = ui_module
397
398 # Ensure the path to bitbake's bin directory is in PATH so that things like
399 # bitbake-worker can be run (usually this is the case, but it doesn't have to be)
400 path = os.getenv('PATH').split(':')
401 bitbakebinpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'bin'))
402 for entry in path:
403 if entry.endswith(os.sep):
404 entry = entry[:-1]
405 if os.path.abspath(entry) == bitbakebinpath:
406 break
407 else:
408 path.insert(0, bitbakebinpath)
409 os.environ['PATH'] = ':'.join(path)
410
411 if self.server_connection:
412 _server_connections.append(self.server_connection)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500413 if config_only:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500414 config_params.updateToServer(self.server_connection.connection, os.environ.copy())
415 self.run_command('parseConfiguration')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500416 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500417 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500418 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500419
Andrew Geissler82c905d2020-04-13 13:39:40 -0500420 self.config_data = TinfoilDataStoreConnector(self, 0)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500421 self.cooker = TinfoilCookerAdapter(self)
422 self.cooker_data = self.cooker.recipecaches['']
423 else:
424 raise Exception('Failed to start bitbake server')
425
426 def run_actions(self, config_params):
427 """
428 Run the actions specified in config_params through the UI.
429 """
430 ret = self.ui_module.main(self.server_connection.connection, self.server_connection.events, config_params)
431 if ret:
432 raise TinfoilUIException(ret)
433
434 def parseRecipes(self):
435 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500436 Legacy function - use parse_recipes() instead.
437 """
438 self.parse_recipes()
439
440 def parse_recipes(self):
441 """
442 Load information on all recipes. Normally you should specify
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500443 config_only=False when calling prepare() instead of using this
444 function; this function is designed for situations where you need
445 to initialise Tinfoil and use it with config_only=True first and
446 then conditionally call this function to parse recipes later.
447 """
Andrew Geissler95ac1b82021-03-31 14:34:31 -0500448 config_params = TinfoilConfigParameters(config_only=False, quiet=self.quiet)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500449 self.run_actions(config_params)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500450 self.recipes_parsed = True
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500451
Andrew Geissler9aee5002022-03-30 16:27:02 +0000452 def run_command(self, command, *params, handle_events=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500453 """
454 Run a command on the server (as implemented in bb.command).
455 Note that there are two types of command - synchronous and
456 asynchronous; in order to receive the results of asynchronous
457 commands you will need to set an appropriate event mask
458 using set_event_mask() and listen for the result using
459 wait_event() - with the correct event mask you'll at least get
460 bb.command.CommandCompleted and possibly other events before
461 that depending on the command.
462 """
463 if not self.server_connection:
464 raise Exception('Not connected to server (did you call .prepare()?)')
465
466 commandline = [command]
467 if params:
468 commandline.extend(params)
Andrew Geisslerf0343792020-11-18 10:42:21 -0600469 try:
470 result = self.server_connection.connection.runCommand(commandline)
471 finally:
Andrew Geissler9aee5002022-03-30 16:27:02 +0000472 while handle_events:
Andrew Geisslerf0343792020-11-18 10:42:21 -0600473 event = self.wait_event()
474 if not event:
475 break
476 if isinstance(event, logging.LogRecord):
477 if event.taskpid == 0 or event.levelno > logging.INFO:
478 self.logger.handle(event)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500479 if result[1]:
480 raise TinfoilCommandFailed(result[1])
481 return result[0]
482
483 def set_event_mask(self, eventlist):
484 """Set the event mask which will be applied within wait_event()"""
485 if not self.server_connection:
486 raise Exception('Not connected to server (did you call .prepare()?)')
487 llevel, debug_domains = bb.msg.constructLogOptions()
488 ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
489 if not ret:
490 raise Exception('setEventMask failed')
491
492 def wait_event(self, timeout=0):
493 """
494 Wait for an event from the server for the specified time.
495 A timeout of 0 means don't wait if there are no events in the queue.
496 Returns the next event in the queue or None if the timeout was
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000497 reached. Note that in order to receive any events you will
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500498 first need to set the internal event mask using set_event_mask()
499 (otherwise whatever event mask the UI set up will be in effect).
500 """
501 if not self.server_connection:
502 raise Exception('Not connected to server (did you call .prepare()?)')
503 return self.server_connection.events.waitEvent(timeout)
504
Andrew Geissler5a43b432020-06-13 10:46:56 -0500505 def get_overlayed_recipes(self, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500506 """
507 Find recipes which are overlayed (i.e. where recipes exist in multiple layers)
508 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500509 return defaultdict(list, self.run_command('getOverlayedRecipes', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500510
511 def get_skipped_recipes(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500512 """
513 Find recipes which were skipped (i.e. SkipRecipe was raised
514 during parsing).
515 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500516 return OrderedDict(self.run_command('getSkippedRecipes'))
517
Andrew Geissler82c905d2020-04-13 13:39:40 -0500518 def get_all_providers(self, mc=''):
519 return defaultdict(list, self.run_command('allProviders', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500520
Andrew Geissler82c905d2020-04-13 13:39:40 -0500521 def find_providers(self, mc=''):
522 return self.run_command('findProviders', mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500523
524 def find_best_provider(self, pn):
525 return self.run_command('findBestProvider', pn)
526
527 def get_runtime_providers(self, rdep):
528 return self.run_command('getRuntimeProviders', rdep)
529
530 def get_recipe_file(self, pn):
531 """
532 Get the file name for the specified recipe/target. Raises
533 bb.providers.NoProvider if there is no match or the recipe was
534 skipped.
535 """
536 best = self.find_best_provider(pn)
537 if not best or (len(best) > 3 and not best[3]):
538 skiplist = self.get_skipped_recipes()
539 taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
540 skipreasons = taskdata.get_reasons(pn)
541 if skipreasons:
542 raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons)))
543 else:
544 raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
545 return best[3]
546
Andrew Geissler5a43b432020-06-13 10:46:56 -0500547 def get_file_appends(self, fn, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500548 """
549 Find the bbappends for a recipe file
550 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500551 return self.run_command('getFileAppends', fn, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500552
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500553 def all_recipes(self, mc='', sort=True):
554 """
555 Enable iterating over all recipes in the current configuration.
556 Returns an iterator over TinfoilRecipeInfo objects created on demand.
557 Parameters:
558 mc: The multiconfig, default of '' uses the main configuration.
559 sort: True to sort recipes alphabetically (default), False otherwise
560 """
561 recipecache = self.cooker.recipecaches[mc]
562 if sort:
563 recipes = sorted(recipecache.pkg_pn.items())
564 else:
565 recipes = recipecache.pkg_pn.items()
566 for pn, fns in recipes:
567 prov = self.find_best_provider(pn)
568 recipe = TinfoilRecipeInfo(recipecache,
569 self.config_data,
570 pn=pn,
571 fn=prov[3],
572 fns=fns)
573 yield recipe
574
575 def all_recipe_files(self, mc='', variants=True, preferred_only=False):
576 """
577 Enable iterating over all recipe files in the current configuration.
578 Returns an iterator over file paths.
579 Parameters:
580 mc: The multiconfig, default of '' uses the main configuration.
581 variants: True to include variants of recipes created through
582 BBCLASSEXTEND (default) or False to exclude them
583 preferred_only: True to include only the preferred recipe where
584 multiple exist providing the same PN, False to list
585 all recipes
586 """
587 recipecache = self.cooker.recipecaches[mc]
588 if preferred_only:
589 files = []
590 for pn in recipecache.pkg_pn.keys():
591 prov = self.find_best_provider(pn)
592 files.append(prov[3])
593 else:
594 files = recipecache.pkg_fn.keys()
595 for fn in sorted(files):
596 if not variants and fn.startswith('virtual:'):
597 continue
598 yield fn
599
600
601 def get_recipe_info(self, pn, mc=''):
602 """
603 Get information on a specific recipe in the current configuration by name (PN).
604 Returns a TinfoilRecipeInfo object created on demand.
605 Parameters:
606 mc: The multiconfig, default of '' uses the main configuration.
607 """
608 recipecache = self.cooker.recipecaches[mc]
609 prov = self.find_best_provider(pn)
610 fn = prov[3]
Brad Bishop316dfdd2018-06-25 12:45:53 -0400611 if fn:
612 actual_pn = recipecache.pkg_fn[fn]
613 recipe = TinfoilRecipeInfo(recipecache,
614 self.config_data,
615 pn=actual_pn,
616 fn=fn,
617 fns=recipecache.pkg_pn[actual_pn])
618 return recipe
619 else:
620 return None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500621
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500622 def parse_recipe(self, pn):
623 """
624 Parse the specified recipe and return a datastore object
625 representing the environment for the recipe.
626 """
627 fn = self.get_recipe_file(pn)
628 return self.parse_recipe_file(fn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500629
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600630 def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
631 """
632 Parse the specified recipe file (with or without bbappends)
633 and return a datastore object representing the environment
634 for the recipe.
635 Parameters:
636 fn: recipe file to parse - can be a file path or virtual
637 specification
638 appends: True to apply bbappends, False otherwise
639 appendlist: optional list of bbappend files to apply, if you
640 want to filter them
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600641 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500642 if self.tracking:
643 # Enable history tracking just for the parse operation
644 self.run_command('enableDataTracking')
645 try:
646 if appends and appendlist == []:
647 appends = False
648 if config_data:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500649 config_data = bb.data.createCopy(config_data)
650 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500651 else:
652 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
653 if dscon:
654 return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
655 else:
656 return None
657 finally:
658 if self.tracking:
659 self.run_command('disableDataTracking')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500660
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500661 def build_file(self, buildfile, task, internal=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500662 """
663 Runs the specified task for just a single recipe (i.e. no dependencies).
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500664 This is equivalent to bitbake -b, except with the default internal=True
665 no warning about dependencies will be produced, normal info messages
666 from the runqueue will be silenced and BuildInit, BuildStarted and
667 BuildCompleted events will not be fired.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500668 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500669 return self.run_command('buildFile', buildfile, task, internal)
670
671 def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None):
672 """
673 Builds the specified targets. This is equivalent to a normal invocation
674 of bitbake. Has built-in event handling which is enabled by default and
675 can be extended if needed.
676 Parameters:
677 targets:
678 One or more targets to build. Can be a list or a
679 space-separated string.
680 task:
681 The task to run; if None then the value of BB_DEFAULT_TASK
682 will be used. Default None.
683 handle_events:
684 True to handle events in a similar way to normal bitbake
685 invocation with knotty; False to return immediately (on the
686 assumption that the caller will handle the events instead).
687 Default True.
688 extra_events:
689 An optional list of events to add to the event mask (if
690 handle_events=True). If you add events here you also need
691 to specify a callback function in event_callback that will
692 handle the additional events. Default None.
693 event_callback:
694 An optional function taking a single parameter which
695 will be called first upon receiving any event (if
696 handle_events=True) so that the caller can override or
697 extend the event handling. Default None.
698 """
699 if isinstance(targets, str):
700 targets = targets.split()
701 if not task:
702 task = self.config_data.getVar('BB_DEFAULT_TASK')
703
704 if handle_events:
705 # A reasonable set of default events matching up with those we handle below
706 eventmask = [
707 'bb.event.BuildStarted',
708 'bb.event.BuildCompleted',
709 'logging.LogRecord',
710 'bb.event.NoProvider',
711 'bb.command.CommandCompleted',
712 'bb.command.CommandFailed',
713 'bb.build.TaskStarted',
714 'bb.build.TaskFailed',
715 'bb.build.TaskSucceeded',
716 'bb.build.TaskFailedSilent',
717 'bb.build.TaskProgress',
718 'bb.runqueue.runQueueTaskStarted',
719 'bb.runqueue.sceneQueueTaskStarted',
720 'bb.event.ProcessStarted',
721 'bb.event.ProcessProgress',
722 'bb.event.ProcessFinished',
723 ]
724 if extra_events:
725 eventmask.extend(extra_events)
726 ret = self.set_event_mask(eventmask)
727
728 includelogs = self.config_data.getVar('BBINCLUDELOGS')
729 loglines = self.config_data.getVar('BBINCLUDELOGS_LINES')
730
731 ret = self.run_command('buildTargets', targets, task)
732 if handle_events:
Andrew Geissler517393d2023-01-13 08:55:19 -0600733 lastevent = time.time()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500734 result = False
735 # Borrowed from knotty, instead somewhat hackily we use the helper
736 # as the object to store "shutdown" on
737 helper = bb.ui.uihelper.BBUIHelper()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500738 helper.shutdown = 0
739 parseprogress = None
Andrew Geissler82c905d2020-04-13 13:39:40 -0500740 termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500741 try:
742 while True:
743 try:
744 event = self.wait_event(0.25)
745 if event:
Andrew Geissler517393d2023-01-13 08:55:19 -0600746 lastevent = time.time()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500747 if event_callback and event_callback(event):
748 continue
749 if helper.eventHandler(event):
750 if isinstance(event, bb.build.TaskFailedSilent):
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500751 self.logger.warning("Logfile for failed setscene task is %s" % event.logfile)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500752 elif isinstance(event, bb.build.TaskFailed):
753 bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter)
754 continue
755 if isinstance(event, bb.event.ProcessStarted):
756 if self.quiet > 1:
757 continue
758 parseprogress = bb.ui.knotty.new_progress(event.processname, event.total)
759 parseprogress.start(False)
760 continue
761 if isinstance(event, bb.event.ProcessProgress):
762 if self.quiet > 1:
763 continue
764 if parseprogress:
765 parseprogress.update(event.progress)
766 else:
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000767 bb.warn("Got ProcessProgress event for something that never started?")
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500768 continue
769 if isinstance(event, bb.event.ProcessFinished):
770 if self.quiet > 1:
771 continue
772 if parseprogress:
773 parseprogress.finish()
774 parseprogress = None
775 continue
776 if isinstance(event, bb.command.CommandCompleted):
777 result = True
778 break
Andrew Geissler517393d2023-01-13 08:55:19 -0600779 if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500780 self.logger.error(str(event))
781 result = False
782 break
783 if isinstance(event, logging.LogRecord):
784 if event.taskpid == 0 or event.levelno > logging.INFO:
785 self.logger.handle(event)
786 continue
787 if isinstance(event, bb.event.NoProvider):
788 self.logger.error(str(event))
789 result = False
790 break
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500791 elif helper.shutdown > 1:
792 break
793 termfilter.updateFooter()
Andrew Geissler517393d2023-01-13 08:55:19 -0600794 if time.time() > (lastevent + (3*60)):
795 if not self.run_command('ping', handle_events=False):
796 print("\nUnable to ping server and no events, closing down...\n")
797 return False
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500798 except KeyboardInterrupt:
799 termfilter.clearFooter()
800 if helper.shutdown == 1:
801 print("\nSecond Keyboard Interrupt, stopping...\n")
802 ret = self.run_command("stateForceShutdown")
803 if ret and ret[2]:
804 self.logger.error("Unable to cleanly stop: %s" % ret[2])
805 elif helper.shutdown == 0:
806 print("\nKeyboard Interrupt, closing down...\n")
807 interrupted = True
808 ret = self.run_command("stateShutdown")
809 if ret and ret[2]:
810 self.logger.error("Unable to cleanly shutdown: %s" % ret[2])
811 helper.shutdown = helper.shutdown + 1
812 termfilter.clearFooter()
813 finally:
814 termfilter.finish()
815 if helper.failed_tasks:
816 result = False
817 return result
818 else:
819 return ret
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600820
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500821 def shutdown(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500822 """
823 Shut down tinfoil. Disconnects from the server and gracefully
824 releases any associated resources. You must call this function if
825 prepare() has been called, or use a with... block when you create
826 the tinfoil object which will ensure that it gets called.
827 """
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500828 try:
829 if self.server_connection:
830 try:
831 self.run_command('clientComplete')
832 finally:
833 _server_connections.remove(self.server_connection)
834 bb.event.ui_queue = []
835 self.server_connection.terminate()
836 self.server_connection = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500837
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500838 finally:
839 # Restore logging handlers to how it looked when we started
840 if self.oldhandlers:
841 for handler in self.logger.handlers:
842 if handler not in self.oldhandlers:
843 self.logger.handlers.remove(handler)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500844
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500845 def _reconvert_type(self, obj, origtypename):
846 """
847 Convert an object back to the right type, in the case
848 that marshalling has changed it (especially with xmlrpc)
849 """
850 supported_types = {
851 'set': set,
852 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
853 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500854
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500855 origtype = supported_types.get(origtypename, None)
856 if origtype is None:
857 raise Exception('Unsupported type "%s"' % origtypename)
858 if type(obj) == origtype:
859 newobj = obj
860 elif isinstance(obj, dict):
861 # New style class
862 newobj = origtype()
863 for k,v in obj.items():
864 setattr(newobj, k, v)
865 else:
866 # Assume we can coerce the type
867 newobj = origtype(obj)
868
869 if isinstance(newobj, bb.command.DataStoreConnectionHandle):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500870 newobj = TinfoilDataStoreConnector(self, newobj.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500871
872 return newobj
873
874
875class TinfoilConfigParameters(BitBakeConfigParameters):
876
877 def __init__(self, config_only, **options):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500878 self.initial_options = options
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500879 # Apply some sane defaults
880 if not 'parse_only' in options:
881 self.initial_options['parse_only'] = not config_only
882 #if not 'status_only' in options:
883 # self.initial_options['status_only'] = config_only
884 if not 'ui' in options:
885 self.initial_options['ui'] = 'knotty'
886 if not 'argv' in options:
887 self.initial_options['argv'] = []
888
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500889 super(TinfoilConfigParameters, self).__init__()
890
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500891 def parseCommandLine(self, argv=None):
892 # We don't want any parameters parsed from the command line
893 opts = super(TinfoilConfigParameters, self).parseCommandLine([])
894 for key, val in self.initial_options.items():
895 setattr(opts[0], key, val)
896 return opts