blob: dcd3910cc430a6c9df64f6932e5c3e627c6a02cc [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[:]
Andrew Geissler220dafd2023-10-04 10:18:08 -0500328 self.localhandlers = []
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500329 if setup_logging:
330 # This is the *client-side* logger, nothing to do with
331 # logging messages from the server
332 bb.msg.logger_create('BitBake', output)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500333 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 Geissler220dafd2023-10-04 10:18:08 -0500452 def modified_files(self):
453 """
454 Notify the server it needs to revalidate it's caches since the client has modified files
455 """
456 self.run_command("revalidateCaches")
457
Andrew Geissler9aee5002022-03-30 16:27:02 +0000458 def run_command(self, command, *params, handle_events=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500459 """
460 Run a command on the server (as implemented in bb.command).
461 Note that there are two types of command - synchronous and
462 asynchronous; in order to receive the results of asynchronous
463 commands you will need to set an appropriate event mask
464 using set_event_mask() and listen for the result using
465 wait_event() - with the correct event mask you'll at least get
466 bb.command.CommandCompleted and possibly other events before
467 that depending on the command.
468 """
469 if not self.server_connection:
470 raise Exception('Not connected to server (did you call .prepare()?)')
471
472 commandline = [command]
473 if params:
474 commandline.extend(params)
Andrew Geisslerf0343792020-11-18 10:42:21 -0600475 try:
476 result = self.server_connection.connection.runCommand(commandline)
477 finally:
Andrew Geissler9aee5002022-03-30 16:27:02 +0000478 while handle_events:
Andrew Geisslerf0343792020-11-18 10:42:21 -0600479 event = self.wait_event()
480 if not event:
481 break
482 if isinstance(event, logging.LogRecord):
483 if event.taskpid == 0 or event.levelno > logging.INFO:
484 self.logger.handle(event)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500485 if result[1]:
486 raise TinfoilCommandFailed(result[1])
487 return result[0]
488
489 def set_event_mask(self, eventlist):
490 """Set the event mask which will be applied within wait_event()"""
491 if not self.server_connection:
492 raise Exception('Not connected to server (did you call .prepare()?)')
493 llevel, debug_domains = bb.msg.constructLogOptions()
494 ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
495 if not ret:
496 raise Exception('setEventMask failed')
497
498 def wait_event(self, timeout=0):
499 """
500 Wait for an event from the server for the specified time.
501 A timeout of 0 means don't wait if there are no events in the queue.
502 Returns the next event in the queue or None if the timeout was
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000503 reached. Note that in order to receive any events you will
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500504 first need to set the internal event mask using set_event_mask()
505 (otherwise whatever event mask the UI set up will be in effect).
506 """
507 if not self.server_connection:
508 raise Exception('Not connected to server (did you call .prepare()?)')
509 return self.server_connection.events.waitEvent(timeout)
510
Andrew Geissler5a43b432020-06-13 10:46:56 -0500511 def get_overlayed_recipes(self, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500512 """
513 Find recipes which are overlayed (i.e. where recipes exist in multiple layers)
514 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500515 return defaultdict(list, self.run_command('getOverlayedRecipes', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500516
517 def get_skipped_recipes(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500518 """
519 Find recipes which were skipped (i.e. SkipRecipe was raised
520 during parsing).
521 """
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500522 return OrderedDict(self.run_command('getSkippedRecipes'))
523
Andrew Geissler82c905d2020-04-13 13:39:40 -0500524 def get_all_providers(self, mc=''):
525 return defaultdict(list, self.run_command('allProviders', mc))
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500526
Andrew Geissler82c905d2020-04-13 13:39:40 -0500527 def find_providers(self, mc=''):
528 return self.run_command('findProviders', mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500529
530 def find_best_provider(self, pn):
531 return self.run_command('findBestProvider', pn)
532
533 def get_runtime_providers(self, rdep):
534 return self.run_command('getRuntimeProviders', rdep)
535
536 def get_recipe_file(self, pn):
537 """
538 Get the file name for the specified recipe/target. Raises
539 bb.providers.NoProvider if there is no match or the recipe was
540 skipped.
541 """
542 best = self.find_best_provider(pn)
543 if not best or (len(best) > 3 and not best[3]):
544 skiplist = self.get_skipped_recipes()
545 taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
546 skipreasons = taskdata.get_reasons(pn)
547 if skipreasons:
548 raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons)))
549 else:
550 raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
551 return best[3]
552
Andrew Geissler5a43b432020-06-13 10:46:56 -0500553 def get_file_appends(self, fn, mc=''):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500554 """
555 Find the bbappends for a recipe file
556 """
Andrew Geissler5a43b432020-06-13 10:46:56 -0500557 return self.run_command('getFileAppends', fn, mc)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500558
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500559 def all_recipes(self, mc='', sort=True):
560 """
561 Enable iterating over all recipes in the current configuration.
562 Returns an iterator over TinfoilRecipeInfo objects created on demand.
563 Parameters:
564 mc: The multiconfig, default of '' uses the main configuration.
565 sort: True to sort recipes alphabetically (default), False otherwise
566 """
567 recipecache = self.cooker.recipecaches[mc]
568 if sort:
569 recipes = sorted(recipecache.pkg_pn.items())
570 else:
571 recipes = recipecache.pkg_pn.items()
572 for pn, fns in recipes:
573 prov = self.find_best_provider(pn)
574 recipe = TinfoilRecipeInfo(recipecache,
575 self.config_data,
576 pn=pn,
577 fn=prov[3],
578 fns=fns)
579 yield recipe
580
581 def all_recipe_files(self, mc='', variants=True, preferred_only=False):
582 """
583 Enable iterating over all recipe files in the current configuration.
584 Returns an iterator over file paths.
585 Parameters:
586 mc: The multiconfig, default of '' uses the main configuration.
587 variants: True to include variants of recipes created through
588 BBCLASSEXTEND (default) or False to exclude them
589 preferred_only: True to include only the preferred recipe where
590 multiple exist providing the same PN, False to list
591 all recipes
592 """
593 recipecache = self.cooker.recipecaches[mc]
594 if preferred_only:
595 files = []
596 for pn in recipecache.pkg_pn.keys():
597 prov = self.find_best_provider(pn)
598 files.append(prov[3])
599 else:
600 files = recipecache.pkg_fn.keys()
601 for fn in sorted(files):
602 if not variants and fn.startswith('virtual:'):
603 continue
604 yield fn
605
606
607 def get_recipe_info(self, pn, mc=''):
608 """
609 Get information on a specific recipe in the current configuration by name (PN).
610 Returns a TinfoilRecipeInfo object created on demand.
611 Parameters:
612 mc: The multiconfig, default of '' uses the main configuration.
613 """
614 recipecache = self.cooker.recipecaches[mc]
615 prov = self.find_best_provider(pn)
616 fn = prov[3]
Brad Bishop316dfdd2018-06-25 12:45:53 -0400617 if fn:
618 actual_pn = recipecache.pkg_fn[fn]
619 recipe = TinfoilRecipeInfo(recipecache,
620 self.config_data,
621 pn=actual_pn,
622 fn=fn,
623 fns=recipecache.pkg_pn[actual_pn])
624 return recipe
625 else:
626 return None
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500627
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500628 def parse_recipe(self, pn):
629 """
630 Parse the specified recipe and return a datastore object
631 representing the environment for the recipe.
632 """
633 fn = self.get_recipe_file(pn)
634 return self.parse_recipe_file(fn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500635
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600636 def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
637 """
638 Parse the specified recipe file (with or without bbappends)
639 and return a datastore object representing the environment
640 for the recipe.
641 Parameters:
642 fn: recipe file to parse - can be a file path or virtual
643 specification
644 appends: True to apply bbappends, False otherwise
645 appendlist: optional list of bbappend files to apply, if you
646 want to filter them
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600647 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500648 if self.tracking:
649 # Enable history tracking just for the parse operation
650 self.run_command('enableDataTracking')
651 try:
652 if appends and appendlist == []:
653 appends = False
654 if config_data:
Andrew Geissler82c905d2020-04-13 13:39:40 -0500655 config_data = bb.data.createCopy(config_data)
656 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500657 else:
658 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
659 if dscon:
660 return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
661 else:
662 return None
663 finally:
664 if self.tracking:
665 self.run_command('disableDataTracking')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500666
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500667 def build_file(self, buildfile, task, internal=True):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500668 """
669 Runs the specified task for just a single recipe (i.e. no dependencies).
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500670 This is equivalent to bitbake -b, except with the default internal=True
671 no warning about dependencies will be produced, normal info messages
672 from the runqueue will be silenced and BuildInit, BuildStarted and
673 BuildCompleted events will not be fired.
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500674 """
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500675 return self.run_command('buildFile', buildfile, task, internal)
676
677 def build_targets(self, targets, task=None, handle_events=True, extra_events=None, event_callback=None):
678 """
679 Builds the specified targets. This is equivalent to a normal invocation
680 of bitbake. Has built-in event handling which is enabled by default and
681 can be extended if needed.
682 Parameters:
683 targets:
684 One or more targets to build. Can be a list or a
685 space-separated string.
686 task:
687 The task to run; if None then the value of BB_DEFAULT_TASK
688 will be used. Default None.
689 handle_events:
690 True to handle events in a similar way to normal bitbake
691 invocation with knotty; False to return immediately (on the
692 assumption that the caller will handle the events instead).
693 Default True.
694 extra_events:
695 An optional list of events to add to the event mask (if
696 handle_events=True). If you add events here you also need
697 to specify a callback function in event_callback that will
698 handle the additional events. Default None.
699 event_callback:
700 An optional function taking a single parameter which
701 will be called first upon receiving any event (if
702 handle_events=True) so that the caller can override or
703 extend the event handling. Default None.
704 """
705 if isinstance(targets, str):
706 targets = targets.split()
707 if not task:
708 task = self.config_data.getVar('BB_DEFAULT_TASK')
709
710 if handle_events:
711 # A reasonable set of default events matching up with those we handle below
712 eventmask = [
713 'bb.event.BuildStarted',
714 'bb.event.BuildCompleted',
715 'logging.LogRecord',
716 'bb.event.NoProvider',
717 'bb.command.CommandCompleted',
718 'bb.command.CommandFailed',
719 'bb.build.TaskStarted',
720 'bb.build.TaskFailed',
721 'bb.build.TaskSucceeded',
722 'bb.build.TaskFailedSilent',
723 'bb.build.TaskProgress',
724 'bb.runqueue.runQueueTaskStarted',
725 'bb.runqueue.sceneQueueTaskStarted',
726 'bb.event.ProcessStarted',
727 'bb.event.ProcessProgress',
728 'bb.event.ProcessFinished',
729 ]
730 if extra_events:
731 eventmask.extend(extra_events)
732 ret = self.set_event_mask(eventmask)
733
734 includelogs = self.config_data.getVar('BBINCLUDELOGS')
735 loglines = self.config_data.getVar('BBINCLUDELOGS_LINES')
736
737 ret = self.run_command('buildTargets', targets, task)
738 if handle_events:
Andrew Geissler517393d2023-01-13 08:55:19 -0600739 lastevent = time.time()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500740 result = False
741 # Borrowed from knotty, instead somewhat hackily we use the helper
742 # as the object to store "shutdown" on
743 helper = bb.ui.uihelper.BBUIHelper()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500744 helper.shutdown = 0
745 parseprogress = None
Andrew Geissler82c905d2020-04-13 13:39:40 -0500746 termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500747 try:
748 while True:
749 try:
750 event = self.wait_event(0.25)
751 if event:
Andrew Geissler517393d2023-01-13 08:55:19 -0600752 lastevent = time.time()
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500753 if event_callback and event_callback(event):
754 continue
755 if helper.eventHandler(event):
756 if isinstance(event, bb.build.TaskFailedSilent):
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500757 self.logger.warning("Logfile for failed setscene task is %s" % event.logfile)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500758 elif isinstance(event, bb.build.TaskFailed):
759 bb.ui.knotty.print_event_log(event, includelogs, loglines, termfilter)
760 continue
761 if isinstance(event, bb.event.ProcessStarted):
762 if self.quiet > 1:
763 continue
764 parseprogress = bb.ui.knotty.new_progress(event.processname, event.total)
765 parseprogress.start(False)
766 continue
767 if isinstance(event, bb.event.ProcessProgress):
768 if self.quiet > 1:
769 continue
770 if parseprogress:
771 parseprogress.update(event.progress)
772 else:
Andrew Geissler7e0e3c02022-02-25 20:34:39 +0000773 bb.warn("Got ProcessProgress event for something that never started?")
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500774 continue
775 if isinstance(event, bb.event.ProcessFinished):
776 if self.quiet > 1:
777 continue
778 if parseprogress:
779 parseprogress.finish()
780 parseprogress = None
781 continue
782 if isinstance(event, bb.command.CommandCompleted):
783 result = True
784 break
Andrew Geissler517393d2023-01-13 08:55:19 -0600785 if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500786 self.logger.error(str(event))
787 result = False
788 break
789 if isinstance(event, logging.LogRecord):
790 if event.taskpid == 0 or event.levelno > logging.INFO:
791 self.logger.handle(event)
792 continue
793 if isinstance(event, bb.event.NoProvider):
794 self.logger.error(str(event))
795 result = False
796 break
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500797 elif helper.shutdown > 1:
798 break
799 termfilter.updateFooter()
Andrew Geissler517393d2023-01-13 08:55:19 -0600800 if time.time() > (lastevent + (3*60)):
801 if not self.run_command('ping', handle_events=False):
802 print("\nUnable to ping server and no events, closing down...\n")
803 return False
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500804 except KeyboardInterrupt:
805 termfilter.clearFooter()
806 if helper.shutdown == 1:
807 print("\nSecond Keyboard Interrupt, stopping...\n")
808 ret = self.run_command("stateForceShutdown")
809 if ret and ret[2]:
810 self.logger.error("Unable to cleanly stop: %s" % ret[2])
811 elif helper.shutdown == 0:
812 print("\nKeyboard Interrupt, closing down...\n")
813 interrupted = True
814 ret = self.run_command("stateShutdown")
815 if ret and ret[2]:
816 self.logger.error("Unable to cleanly shutdown: %s" % ret[2])
817 helper.shutdown = helper.shutdown + 1
818 termfilter.clearFooter()
819 finally:
820 termfilter.finish()
821 if helper.failed_tasks:
822 result = False
823 return result
824 else:
825 return ret
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600826
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500827 def shutdown(self):
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500828 """
829 Shut down tinfoil. Disconnects from the server and gracefully
830 releases any associated resources. You must call this function if
831 prepare() has been called, or use a with... block when you create
832 the tinfoil object which will ensure that it gets called.
833 """
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500834 try:
835 if self.server_connection:
836 try:
837 self.run_command('clientComplete')
838 finally:
839 _server_connections.remove(self.server_connection)
840 bb.event.ui_queue = []
841 self.server_connection.terminate()
842 self.server_connection = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500843
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500844 finally:
845 # Restore logging handlers to how it looked when we started
846 if self.oldhandlers:
847 for handler in self.logger.handlers:
848 if handler not in self.oldhandlers:
849 self.logger.handlers.remove(handler)
Brad Bishopd7bf8c12018-02-25 22:55:05 -0500850
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500851 def _reconvert_type(self, obj, origtypename):
852 """
853 Convert an object back to the right type, in the case
854 that marshalling has changed it (especially with xmlrpc)
855 """
856 supported_types = {
857 'set': set,
858 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
859 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500860
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500861 origtype = supported_types.get(origtypename, None)
862 if origtype is None:
863 raise Exception('Unsupported type "%s"' % origtypename)
864 if type(obj) == origtype:
865 newobj = obj
866 elif isinstance(obj, dict):
867 # New style class
868 newobj = origtype()
869 for k,v in obj.items():
870 setattr(newobj, k, v)
871 else:
872 # Assume we can coerce the type
873 newobj = origtype(obj)
874
875 if isinstance(newobj, bb.command.DataStoreConnectionHandle):
Andrew Geissler82c905d2020-04-13 13:39:40 -0500876 newobj = TinfoilDataStoreConnector(self, newobj.dsindex)
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500877
878 return newobj
879
880
881class TinfoilConfigParameters(BitBakeConfigParameters):
882
883 def __init__(self, config_only, **options):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500884 self.initial_options = options
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500885 # Apply some sane defaults
886 if not 'parse_only' in options:
887 self.initial_options['parse_only'] = not config_only
888 #if not 'status_only' in options:
889 # self.initial_options['status_only'] = config_only
890 if not 'ui' in options:
891 self.initial_options['ui'] = 'knotty'
892 if not 'argv' in options:
893 self.initial_options['argv'] = []
894
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500895 super(TinfoilConfigParameters, self).__init__()
896
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500897 def parseCommandLine(self, argv=None):
898 # We don't want any parameters parsed from the command line
899 opts = super(TinfoilConfigParameters, self).parseCommandLine([])
900 for key, val in self.initial_options.items():
901 setattr(opts[0], key, val)
902 return opts