Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 1 | # tinfoil: a simple wrapper around cooker for bitbake-based command-line utilities |
| 2 | # |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 3 | # Copyright (C) 2012-2017 Intel Corporation |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 4 | # Copyright (C) 2011 Mentor Graphics Corporation |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 5 | # Copyright (C) 2006-2012 Richard Purdie |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 6 | # |
Brad Bishop | c342db3 | 2019-05-15 21:57:59 -0400 | [diff] [blame] | 7 | # SPDX-License-Identifier: GPL-2.0-only |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 8 | # |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 9 | |
| 10 | import logging |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 11 | import os |
| 12 | import sys |
Andrew Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame] | 13 | import time |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 14 | import atexit |
| 15 | import re |
| 16 | from collections import OrderedDict, defaultdict |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 17 | from functools import partial |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 18 | |
| 19 | import bb.cache |
| 20 | import bb.cooker |
| 21 | import bb.providers |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 22 | import bb.taskdata |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 23 | import bb.utils |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 24 | import bb.command |
| 25 | import bb.remotedata |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 26 | from bb.main import setup_bitbake, BitBakeConfigParameters |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 27 | import bb.fetch2 |
| 28 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 29 | |
| 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 = [] |
| 33 | def _terminate_connections(): |
| 34 | for connection in _server_connections: |
| 35 | connection.terminate() |
| 36 | atexit.register(_terminate_connections) |
| 37 | |
| 38 | class 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 | |
| 45 | class TinfoilCommandFailed(Exception): |
| 46 | """Exception raised when run_command fails""" |
| 47 | |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 48 | class 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 Geissler | c926e17 | 2021-05-07 16:11:35 -0500 | [diff] [blame] | 56 | 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 Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 60 | 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 | |
| 68 | class 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 84 | class TinfoilDataStoreConnector: |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 85 | """ |
| 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 90 | |
| 91 | def __init__(self, tinfoil, dsindex): |
| 92 | self.tinfoil = tinfoil |
| 93 | self.dsindex = dsindex |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 94 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 101 | return ret |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 102 | |
| 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 115 | |
| 116 | class 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 Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 124 | def __init__(self, tinfoil, mc=''): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 125 | self.tinfoil = tinfoil |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 126 | self.mc = mc |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 127 | def get_file_appends(self, fn): |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 128 | return self.tinfoil.get_file_appends(fn, self.mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 129 | def __getattr__(self, name): |
| 130 | if name == 'overlayed': |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 131 | return self.tinfoil.get_overlayed_recipes(self.mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 132 | elif name == 'bbappends': |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 133 | return self.tinfoil.run_command('getAllAppends', self.mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 134 | else: |
| 135 | raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name)) |
| 136 | |
| 137 | class TinfoilRecipeCacheAdapter: |
| 138 | """ cooker.recipecache adapter """ |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 139 | def __init__(self, tinfoil, mc=''): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 140 | self.tinfoil = tinfoil |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 141 | self.mc = mc |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 142 | self._cache = {} |
| 143 | |
| 144 | def get_pkg_pn_fn(self): |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 145 | pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes', self.mc) or []) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 146 | 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 Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 164 | attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends', self.mc) or []) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 165 | elif name == 'rundeps': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 166 | attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends', self.mc) or []) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 167 | elif name == 'runrecs': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 168 | attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends', self.mc) or []) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 169 | elif name == 'pkg_pepvpr': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 170 | attrvalue = self.tinfoil.run_command('getRecipeVersions', self.mc) or {} |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 171 | elif name == 'inherits': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 172 | attrvalue = self.tinfoil.run_command('getRecipeInherits', self.mc) or {} |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 173 | elif name == 'bbfile_priority': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 174 | attrvalue = self.tinfoil.run_command('getBbFilePriority', self.mc) or {} |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 175 | elif name == 'pkg_dp': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 176 | attrvalue = self.tinfoil.run_command('getDefaultPreference', self.mc) or {} |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 177 | elif name == 'fn_provides': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 178 | attrvalue = self.tinfoil.run_command('getRecipeProvides', self.mc) or {} |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 179 | elif name == 'packages': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 180 | attrvalue = self.tinfoil.run_command('getRecipePackages', self.mc) or {} |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 181 | elif name == 'packages_dynamic': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 182 | attrvalue = self.tinfoil.run_command('getRecipePackagesDynamic', self.mc) or {} |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 183 | elif name == 'rproviders': |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 184 | attrvalue = self.tinfoil.run_command('getRProviders', self.mc) or {} |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 185 | 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 Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 193 | self.multiconfigs = [''] + (tinfoil.config_data.getVar('BBMULTICONFIG') or '').split() |
| 194 | self.collections = {} |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 195 | self.recipecaches = {} |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 196 | for mc in self.multiconfigs: |
| 197 | self.collections[mc] = self.TinfoilCookerCollectionAdapter(tinfoil, mc) |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 198 | self.recipecaches[mc] = self.TinfoilRecipeCacheAdapter(tinfoil, mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 199 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 223 | class 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 299 | class Tinfoil: |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 300 | """ |
| 301 | Tinfoil - an API for scripts and utilities to query |
| 302 | BitBake internals and perform build operations. |
| 303 | """ |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 304 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 305 | def __init__(self, output=sys.stdout, tracking=False, setup_logging=True): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 306 | """ |
| 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 319 | self.logger = logging.getLogger('BitBake') |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 320 | self.config_data = None |
| 321 | self.cooker = None |
| 322 | self.tracking = tracking |
| 323 | self.ui_module = None |
| 324 | self.server_connection = None |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 325 | self.recipes_parsed = False |
| 326 | self.quiet = 0 |
| 327 | self.oldhandlers = self.logger.handlers[:] |
Andrew Geissler | 220dafd | 2023-10-04 10:18:08 -0500 | [diff] [blame] | 328 | self.localhandlers = [] |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 329 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 333 | for handler in self.logger.handlers: |
| 334 | if handler not in self.oldhandlers: |
| 335 | self.localhandlers.append(handler) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 336 | |
Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 337 | def __enter__(self): |
| 338 | return self |
| 339 | |
| 340 | def __exit__(self, type, value, traceback): |
| 341 | self.shutdown() |
| 342 | |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 343 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 377 | if self.tracking: |
| 378 | extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING] |
| 379 | else: |
| 380 | extrafeatures = [] |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 381 | |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 382 | if extra_features: |
| 383 | extrafeatures += extra_features |
| 384 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 385 | if not config_params: |
| 386 | config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 387 | |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 388 | 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 Geissler | c9f7865 | 2020-09-18 14:11:35 -0500 | [diff] [blame] | 394 | self.server_connection, ui_module = setup_bitbake(config_params, extrafeatures) |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 395 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 396 | 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 413 | if config_only: |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 414 | config_params.updateToServer(self.server_connection.connection, os.environ.copy()) |
| 415 | self.run_command('parseConfiguration') |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 416 | else: |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 417 | self.run_actions(config_params) |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 418 | self.recipes_parsed = True |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 419 | |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 420 | self.config_data = TinfoilDataStoreConnector(self, 0) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 421 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 436 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 443 | 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 Geissler | 95ac1b8 | 2021-03-31 14:34:31 -0500 | [diff] [blame] | 448 | config_params = TinfoilConfigParameters(config_only=False, quiet=self.quiet) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 449 | self.run_actions(config_params) |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 450 | self.recipes_parsed = True |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 451 | |
Andrew Geissler | 220dafd | 2023-10-04 10:18:08 -0500 | [diff] [blame] | 452 | 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 Geissler | 9aee500 | 2022-03-30 16:27:02 +0000 | [diff] [blame] | 458 | def run_command(self, command, *params, handle_events=True): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 459 | """ |
| 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 Geissler | f034379 | 2020-11-18 10:42:21 -0600 | [diff] [blame] | 475 | try: |
| 476 | result = self.server_connection.connection.runCommand(commandline) |
| 477 | finally: |
Andrew Geissler | 9aee500 | 2022-03-30 16:27:02 +0000 | [diff] [blame] | 478 | while handle_events: |
Andrew Geissler | f034379 | 2020-11-18 10:42:21 -0600 | [diff] [blame] | 479 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 485 | 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 Geissler | 7e0e3c0 | 2022-02-25 20:34:39 +0000 | [diff] [blame] | 503 | reached. Note that in order to receive any events you will |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 504 | 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 Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 511 | def get_overlayed_recipes(self, mc=''): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 512 | """ |
| 513 | Find recipes which are overlayed (i.e. where recipes exist in multiple layers) |
| 514 | """ |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 515 | return defaultdict(list, self.run_command('getOverlayedRecipes', mc)) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 516 | |
| 517 | def get_skipped_recipes(self): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 518 | """ |
| 519 | Find recipes which were skipped (i.e. SkipRecipe was raised |
| 520 | during parsing). |
| 521 | """ |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 522 | return OrderedDict(self.run_command('getSkippedRecipes')) |
| 523 | |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 524 | def get_all_providers(self, mc=''): |
| 525 | return defaultdict(list, self.run_command('allProviders', mc)) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 526 | |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 527 | def find_providers(self, mc=''): |
| 528 | return self.run_command('findProviders', mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 529 | |
| 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 Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 553 | def get_file_appends(self, fn, mc=''): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 554 | """ |
| 555 | Find the bbappends for a recipe file |
| 556 | """ |
Andrew Geissler | 5a43b43 | 2020-06-13 10:46:56 -0500 | [diff] [blame] | 557 | return self.run_command('getFileAppends', fn, mc) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 558 | |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 559 | 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 Bishop | 316dfdd | 2018-06-25 12:45:53 -0400 | [diff] [blame] | 617 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 627 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 628 | 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 635 | |
Patrick Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 636 | 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 Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 647 | """ |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 648 | 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 Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 655 | config_data = bb.data.createCopy(config_data) |
| 656 | dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, config_data.dsindex) |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 657 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 666 | |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 667 | def build_file(self, buildfile, task, internal=True): |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 668 | """ |
| 669 | Runs the specified task for just a single recipe (i.e. no dependencies). |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 670 | 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 Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 674 | """ |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 675 | 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 Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame] | 739 | lastevent = time.time() |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 740 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 744 | helper.shutdown = 0 |
| 745 | parseprogress = None |
Andrew Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 746 | termfilter = bb.ui.knotty.TerminalFilter(helper, helper, self.logger.handlers, quiet=self.quiet) |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 747 | try: |
| 748 | while True: |
| 749 | try: |
| 750 | event = self.wait_event(0.25) |
| 751 | if event: |
Andrew Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame] | 752 | lastevent = time.time() |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 753 | if event_callback and event_callback(event): |
| 754 | continue |
| 755 | if helper.eventHandler(event): |
| 756 | if isinstance(event, bb.build.TaskFailedSilent): |
Andrew Geissler | c9f7865 | 2020-09-18 14:11:35 -0500 | [diff] [blame] | 757 | self.logger.warning("Logfile for failed setscene task is %s" % event.logfile) |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 758 | 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 Geissler | 7e0e3c0 | 2022-02-25 20:34:39 +0000 | [diff] [blame] | 773 | bb.warn("Got ProcessProgress event for something that never started?") |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 774 | 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 Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame] | 785 | if isinstance(event, (bb.command.CommandFailed, bb.command.CommandExit)): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 786 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 797 | elif helper.shutdown > 1: |
| 798 | break |
| 799 | termfilter.updateFooter() |
Andrew Geissler | 517393d | 2023-01-13 08:55:19 -0600 | [diff] [blame] | 800 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 804 | 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 Williams | c0f7c04 | 2017-02-23 20:41:17 -0600 | [diff] [blame] | 826 | |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 827 | def shutdown(self): |
Brad Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 828 | """ |
| 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 Geissler | c9f7865 | 2020-09-18 14:11:35 -0500 | [diff] [blame] | 834 | 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 843 | |
Andrew Geissler | c9f7865 | 2020-09-18 14:11:35 -0500 | [diff] [blame] | 844 | 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 Bishop | d7bf8c1 | 2018-02-25 22:55:05 -0500 | [diff] [blame] | 850 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 851 | 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 860 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 861 | 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 Geissler | 82c905d | 2020-04-13 13:39:40 -0500 | [diff] [blame] | 876 | newobj = TinfoilDataStoreConnector(self, newobj.dsindex) |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 877 | |
| 878 | return newobj |
| 879 | |
| 880 | |
| 881 | class TinfoilConfigParameters(BitBakeConfigParameters): |
| 882 | |
| 883 | def __init__(self, config_only, **options): |
Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 884 | self.initial_options = options |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 885 | # 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 Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame] | 895 | super(TinfoilConfigParameters, self).__init__() |
| 896 | |
Brad Bishop | 6e60e8b | 2018-02-01 10:27:11 -0500 | [diff] [blame] | 897 | 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 |