blob: 928333a500fc4fef40457e13415ac78bdb83a0eb [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
5#
6# This program is free software; you can redistribute it and/or modify
7# it under the terms of the GNU General Public License version 2 as
8# published by the Free Software Foundation.
9#
10# This program is distributed in the hope that it will be useful,
11# but WITHOUT ANY WARRANTY; without even the implied warranty of
12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13# GNU General Public License for more details.
14#
15# You should have received a copy of the GNU General Public License along
16# with this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
18
19import logging
Patrick Williamsc124f4f2015-09-15 14:41:29 -050020import os
21import sys
Brad Bishop6e60e8b2018-02-01 10:27:11 -050022import atexit
23import re
24from collections import OrderedDict, defaultdict
Patrick Williamsc124f4f2015-09-15 14:41:29 -050025
26import bb.cache
27import bb.cooker
28import bb.providers
Brad Bishop6e60e8b2018-02-01 10:27:11 -050029import bb.taskdata
Patrick Williamsc124f4f2015-09-15 14:41:29 -050030import bb.utils
Brad Bishop6e60e8b2018-02-01 10:27:11 -050031import bb.command
32import bb.remotedata
Patrick Williamsc124f4f2015-09-15 14:41:29 -050033from bb.cookerdata import CookerConfiguration, ConfigParameters
Brad Bishop6e60e8b2018-02-01 10:27:11 -050034from bb.main import setup_bitbake, BitBakeConfigParameters, BBMainException
Patrick Williamsc124f4f2015-09-15 14:41:29 -050035import bb.fetch2
36
Brad Bishop6e60e8b2018-02-01 10:27:11 -050037
38# We need this in order to shut down the connection to the bitbake server,
39# otherwise the process will never properly exit
40_server_connections = []
41def _terminate_connections():
42 for connection in _server_connections:
43 connection.terminate()
44atexit.register(_terminate_connections)
45
46class TinfoilUIException(Exception):
47 """Exception raised when the UI returns non-zero from its main function"""
48 def __init__(self, returncode):
49 self.returncode = returncode
50 def __repr__(self):
51 return 'UI module main returned %d' % self.returncode
52
53class TinfoilCommandFailed(Exception):
54 """Exception raised when run_command fails"""
55
56class TinfoilDataStoreConnector:
57
58 def __init__(self, tinfoil, dsindex):
59 self.tinfoil = tinfoil
60 self.dsindex = dsindex
61 def getVar(self, name):
62 value = self.tinfoil.run_command('dataStoreConnectorFindVar', self.dsindex, name)
63 overrides = None
64 if isinstance(value, dict):
65 if '_connector_origtype' in value:
66 value['_content'] = self.tinfoil._reconvert_type(value['_content'], value['_connector_origtype'])
67 del value['_connector_origtype']
68 if '_connector_overrides' in value:
69 overrides = value['_connector_overrides']
70 del value['_connector_overrides']
71 return value, overrides
72 def getKeys(self):
73 return set(self.tinfoil.run_command('dataStoreConnectorGetKeys', self.dsindex))
74 def getVarHistory(self, name):
75 return self.tinfoil.run_command('dataStoreConnectorGetVarHistory', self.dsindex, name)
76 def expandPythonRef(self, varname, expr, d):
77 ds = bb.remotedata.RemoteDatastores.transmit_datastore(d)
78 ret = self.tinfoil.run_command('dataStoreConnectorExpandPythonRef', ds, varname, expr)
79 return ret
80 def setVar(self, varname, value):
81 if self.dsindex is None:
82 self.tinfoil.run_command('setVariable', varname, value)
83 else:
84 # Not currently implemented - indicate that setting should
85 # be redirected to local side
86 return True
87 def setVarFlag(self, varname, flagname, value):
88 if self.dsindex is None:
89 self.tinfoil.run_command('dataStoreConnectorSetVarFlag', self.dsindex, varname, flagname, value)
90 else:
91 # Not currently implemented - indicate that setting should
92 # be redirected to local side
93 return True
94 def delVar(self, varname):
95 if self.dsindex is None:
96 self.tinfoil.run_command('dataStoreConnectorDelVar', self.dsindex, varname)
97 else:
98 # Not currently implemented - indicate that setting should
99 # be redirected to local side
100 return True
101 def delVarFlag(self, varname, flagname):
102 if self.dsindex is None:
103 self.tinfoil.run_command('dataStoreConnectorDelVar', self.dsindex, varname, flagname)
104 else:
105 # Not currently implemented - indicate that setting should
106 # be redirected to local side
107 return True
108 def renameVar(self, name, newname):
109 if self.dsindex is None:
110 self.tinfoil.run_command('dataStoreConnectorRenameVar', self.dsindex, name, newname)
111 else:
112 # Not currently implemented - indicate that setting should
113 # be redirected to local side
114 return True
115
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 """
124 def __init__(self, tinfoil):
125 self.tinfoil = tinfoil
126 def get_file_appends(self, fn):
127 return self.tinfoil.get_file_appends(fn)
128 def __getattr__(self, name):
129 if name == 'overlayed':
130 return self.tinfoil.get_overlayed_recipes()
131 elif name == 'bbappends':
132 return self.tinfoil.run_command('getAllAppends')
133 else:
134 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
135
136 class TinfoilRecipeCacheAdapter:
137 """ cooker.recipecache adapter """
138 def __init__(self, tinfoil):
139 self.tinfoil = tinfoil
140 self._cache = {}
141
142 def get_pkg_pn_fn(self):
143 pkg_pn = defaultdict(list, self.tinfoil.run_command('getRecipes') or [])
144 pkg_fn = {}
145 for pn, fnlist in pkg_pn.items():
146 for fn in fnlist:
147 pkg_fn[fn] = pn
148 self._cache['pkg_pn'] = pkg_pn
149 self._cache['pkg_fn'] = pkg_fn
150
151 def __getattr__(self, name):
152 # Grab these only when they are requested since they aren't always used
153 if name in self._cache:
154 return self._cache[name]
155 elif name == 'pkg_pn':
156 self.get_pkg_pn_fn()
157 return self._cache[name]
158 elif name == 'pkg_fn':
159 self.get_pkg_pn_fn()
160 return self._cache[name]
161 elif name == 'deps':
162 attrvalue = defaultdict(list, self.tinfoil.run_command('getRecipeDepends') or [])
163 elif name == 'rundeps':
164 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeDepends') or [])
165 elif name == 'runrecs':
166 attrvalue = defaultdict(lambda: defaultdict(list), self.tinfoil.run_command('getRuntimeRecommends') or [])
167 elif name == 'pkg_pepvpr':
168 attrvalue = self.tinfoil.run_command('getRecipeVersions') or {}
169 elif name == 'inherits':
170 attrvalue = self.tinfoil.run_command('getRecipeInherits') or {}
171 elif name == 'bbfile_priority':
172 attrvalue = self.tinfoil.run_command('getBbFilePriority') or {}
173 elif name == 'pkg_dp':
174 attrvalue = self.tinfoil.run_command('getDefaultPreference') or {}
175 else:
176 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
177
178 self._cache[name] = attrvalue
179 return attrvalue
180
181 def __init__(self, tinfoil):
182 self.tinfoil = tinfoil
183 self.collection = self.TinfoilCookerCollectionAdapter(tinfoil)
184 self.recipecaches = {}
185 # FIXME all machines
186 self.recipecaches[''] = self.TinfoilRecipeCacheAdapter(tinfoil)
187 self._cache = {}
188 def __getattr__(self, name):
189 # Grab these only when they are requested since they aren't always used
190 if name in self._cache:
191 return self._cache[name]
192 elif name == 'skiplist':
193 attrvalue = self.tinfoil.get_skipped_recipes()
194 elif name == 'bbfile_config_priorities':
195 ret = self.tinfoil.run_command('getLayerPriorities')
196 bbfile_config_priorities = []
197 for collection, pattern, regex, pri in ret:
198 bbfile_config_priorities.append((collection, pattern, re.compile(regex), pri))
199
200 attrvalue = bbfile_config_priorities
201 else:
202 raise AttributeError("%s instance has no attribute '%s'" % (self.__class__.__name__, name))
203
204 self._cache[name] = attrvalue
205 return attrvalue
206
207 def findBestProvider(self, pn):
208 return self.tinfoil.find_best_provider(pn)
209
210
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500211class Tinfoil:
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500212
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500213 def __init__(self, output=sys.stdout, tracking=False, setup_logging=True):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500214 self.logger = logging.getLogger('BitBake')
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500215 self.config_data = None
216 self.cooker = None
217 self.tracking = tracking
218 self.ui_module = None
219 self.server_connection = None
220 if setup_logging:
221 # This is the *client-side* logger, nothing to do with
222 # logging messages from the server
223 bb.msg.logger_create('BitBake', output)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500224
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600225 def __enter__(self):
226 return self
227
228 def __exit__(self, type, value, traceback):
229 self.shutdown()
230
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500231 def prepare(self, config_only=False, config_params=None, quiet=0):
232 if self.tracking:
233 extrafeatures = [bb.cooker.CookerFeatures.BASEDATASTORE_TRACKING]
234 else:
235 extrafeatures = []
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500236
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500237 if not config_params:
238 config_params = TinfoilConfigParameters(config_only=config_only, quiet=quiet)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500239
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500240 cookerconfig = CookerConfiguration()
241 cookerconfig.setConfigParameters(config_params)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500242
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500243 server, self.server_connection, ui_module = setup_bitbake(config_params,
244 cookerconfig,
245 extrafeatures)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500246
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500247 self.ui_module = ui_module
248
249 # Ensure the path to bitbake's bin directory is in PATH so that things like
250 # bitbake-worker can be run (usually this is the case, but it doesn't have to be)
251 path = os.getenv('PATH').split(':')
252 bitbakebinpath = os.path.abspath(os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..', 'bin'))
253 for entry in path:
254 if entry.endswith(os.sep):
255 entry = entry[:-1]
256 if os.path.abspath(entry) == bitbakebinpath:
257 break
258 else:
259 path.insert(0, bitbakebinpath)
260 os.environ['PATH'] = ':'.join(path)
261
262 if self.server_connection:
263 _server_connections.append(self.server_connection)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500264 if config_only:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500265 config_params.updateToServer(self.server_connection.connection, os.environ.copy())
266 self.run_command('parseConfiguration')
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500267 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500268 self.run_actions(config_params)
269
270 self.config_data = bb.data.init()
271 connector = TinfoilDataStoreConnector(self, None)
272 self.config_data.setVar('_remote_data', connector)
273 self.cooker = TinfoilCookerAdapter(self)
274 self.cooker_data = self.cooker.recipecaches['']
275 else:
276 raise Exception('Failed to start bitbake server')
277
278 def run_actions(self, config_params):
279 """
280 Run the actions specified in config_params through the UI.
281 """
282 ret = self.ui_module.main(self.server_connection.connection, self.server_connection.events, config_params)
283 if ret:
284 raise TinfoilUIException(ret)
285
286 def parseRecipes(self):
287 """
288 Force a parse of all recipes. Normally you should specify
289 config_only=False when calling prepare() instead of using this
290 function; this function is designed for situations where you need
291 to initialise Tinfoil and use it with config_only=True first and
292 then conditionally call this function to parse recipes later.
293 """
294 config_params = TinfoilConfigParameters(config_only=False)
295 self.run_actions(config_params)
296
297 def run_command(self, command, *params):
298 """
299 Run a command on the server (as implemented in bb.command).
300 Note that there are two types of command - synchronous and
301 asynchronous; in order to receive the results of asynchronous
302 commands you will need to set an appropriate event mask
303 using set_event_mask() and listen for the result using
304 wait_event() - with the correct event mask you'll at least get
305 bb.command.CommandCompleted and possibly other events before
306 that depending on the command.
307 """
308 if not self.server_connection:
309 raise Exception('Not connected to server (did you call .prepare()?)')
310
311 commandline = [command]
312 if params:
313 commandline.extend(params)
314 result = self.server_connection.connection.runCommand(commandline)
315 if result[1]:
316 raise TinfoilCommandFailed(result[1])
317 return result[0]
318
319 def set_event_mask(self, eventlist):
320 """Set the event mask which will be applied within wait_event()"""
321 if not self.server_connection:
322 raise Exception('Not connected to server (did you call .prepare()?)')
323 llevel, debug_domains = bb.msg.constructLogOptions()
324 ret = self.run_command('setEventMask', self.server_connection.connection.getEventHandle(), llevel, debug_domains, eventlist)
325 if not ret:
326 raise Exception('setEventMask failed')
327
328 def wait_event(self, timeout=0):
329 """
330 Wait for an event from the server for the specified time.
331 A timeout of 0 means don't wait if there are no events in the queue.
332 Returns the next event in the queue or None if the timeout was
333 reached. Note that in order to recieve any events you will
334 first need to set the internal event mask using set_event_mask()
335 (otherwise whatever event mask the UI set up will be in effect).
336 """
337 if not self.server_connection:
338 raise Exception('Not connected to server (did you call .prepare()?)')
339 return self.server_connection.events.waitEvent(timeout)
340
341 def get_overlayed_recipes(self):
342 return defaultdict(list, self.run_command('getOverlayedRecipes'))
343
344 def get_skipped_recipes(self):
345 return OrderedDict(self.run_command('getSkippedRecipes'))
346
347 def get_all_providers(self):
348 return defaultdict(list, self.run_command('allProviders'))
349
350 def find_providers(self):
351 return self.run_command('findProviders')
352
353 def find_best_provider(self, pn):
354 return self.run_command('findBestProvider', pn)
355
356 def get_runtime_providers(self, rdep):
357 return self.run_command('getRuntimeProviders', rdep)
358
359 def get_recipe_file(self, pn):
360 """
361 Get the file name for the specified recipe/target. Raises
362 bb.providers.NoProvider if there is no match or the recipe was
363 skipped.
364 """
365 best = self.find_best_provider(pn)
366 if not best or (len(best) > 3 and not best[3]):
367 skiplist = self.get_skipped_recipes()
368 taskdata = bb.taskdata.TaskData(None, skiplist=skiplist)
369 skipreasons = taskdata.get_reasons(pn)
370 if skipreasons:
371 raise bb.providers.NoProvider('%s is unavailable:\n %s' % (pn, ' \n'.join(skipreasons)))
372 else:
373 raise bb.providers.NoProvider('Unable to find any recipe file matching "%s"' % pn)
374 return best[3]
375
376 def get_file_appends(self, fn):
377 return self.run_command('getFileAppends', fn)
378
379 def parse_recipe(self, pn):
380 """
381 Parse the specified recipe and return a datastore object
382 representing the environment for the recipe.
383 """
384 fn = self.get_recipe_file(pn)
385 return self.parse_recipe_file(fn)
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500386
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600387 def parse_recipe_file(self, fn, appends=True, appendlist=None, config_data=None):
388 """
389 Parse the specified recipe file (with or without bbappends)
390 and return a datastore object representing the environment
391 for the recipe.
392 Parameters:
393 fn: recipe file to parse - can be a file path or virtual
394 specification
395 appends: True to apply bbappends, False otherwise
396 appendlist: optional list of bbappend files to apply, if you
397 want to filter them
398 config_data: custom config datastore to use. NOTE: if you
399 specify config_data then you cannot use a virtual
400 specification for fn.
401 """
402 if appends and appendlist == []:
403 appends = False
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600404 if config_data:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500405 dctr = bb.remotedata.RemoteDatastores.transmit_datastore(config_data)
406 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist, dctr)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600407 else:
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500408 dscon = self.run_command('parseRecipeFile', fn, appends, appendlist)
409 if dscon:
410 return self._reconvert_type(dscon, 'DataStoreConnectionHandle')
411 else:
412 return None
413
414 def build_file(self, buildfile, task):
415 """
416 Runs the specified task for just a single recipe (i.e. no dependencies).
417 This is equivalent to bitbake -b, except no warning will be printed.
418 """
419 return self.run_command('buildFile', buildfile, task, True)
Patrick Williamsc0f7c042017-02-23 20:41:17 -0600420
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500421 def shutdown(self):
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500422 if self.server_connection:
423 self.run_command('clientComplete')
424 _server_connections.remove(self.server_connection)
425 bb.event.ui_queue = []
426 self.server_connection.terminate()
427 self.server_connection = None
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500428
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500429 def _reconvert_type(self, obj, origtypename):
430 """
431 Convert an object back to the right type, in the case
432 that marshalling has changed it (especially with xmlrpc)
433 """
434 supported_types = {
435 'set': set,
436 'DataStoreConnectionHandle': bb.command.DataStoreConnectionHandle,
437 }
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500438
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500439 origtype = supported_types.get(origtypename, None)
440 if origtype is None:
441 raise Exception('Unsupported type "%s"' % origtypename)
442 if type(obj) == origtype:
443 newobj = obj
444 elif isinstance(obj, dict):
445 # New style class
446 newobj = origtype()
447 for k,v in obj.items():
448 setattr(newobj, k, v)
449 else:
450 # Assume we can coerce the type
451 newobj = origtype(obj)
452
453 if isinstance(newobj, bb.command.DataStoreConnectionHandle):
454 connector = TinfoilDataStoreConnector(self, newobj.dsindex)
455 newobj = bb.data.init()
456 newobj.setVar('_remote_data', connector)
457
458 return newobj
459
460
461class TinfoilConfigParameters(BitBakeConfigParameters):
462
463 def __init__(self, config_only, **options):
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500464 self.initial_options = options
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500465 # Apply some sane defaults
466 if not 'parse_only' in options:
467 self.initial_options['parse_only'] = not config_only
468 #if not 'status_only' in options:
469 # self.initial_options['status_only'] = config_only
470 if not 'ui' in options:
471 self.initial_options['ui'] = 'knotty'
472 if not 'argv' in options:
473 self.initial_options['argv'] = []
474
Patrick Williamsc124f4f2015-09-15 14:41:29 -0500475 super(TinfoilConfigParameters, self).__init__()
476
Brad Bishop6e60e8b2018-02-01 10:27:11 -0500477 def parseCommandLine(self, argv=None):
478 # We don't want any parameters parsed from the command line
479 opts = super(TinfoilConfigParameters, self).parseCommandLine([])
480 for key, val in self.initial_options.items():
481 setattr(opts[0], key, val)
482 return opts