blob: 9ca127b9df046a07533d9e0798da184670175cf0 [file] [log] [blame]
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001# Copyright (C) 2016-2018 Wind River Systems, Inc.
2#
Brad Bishopc342db32019-05-15 21:57:59 -04003# SPDX-License-Identifier: GPL-2.0-only
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08004#
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08005
6import datetime
7
8import logging
9import imp
Andrew Geisslerc9f78652020-09-18 14:11:35 -050010import os
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080011
12from collections import OrderedDict
13from layerindexlib.plugin import LayerIndexPluginUrlError
14
15logger = logging.getLogger('BitBake.layerindexlib')
16
17# Exceptions
18
19class LayerIndexException(Exception):
20 '''LayerIndex Generic Exception'''
21 def __init__(self, message):
22 self.msg = message
23 Exception.__init__(self, message)
24
25 def __str__(self):
26 return self.msg
27
28class LayerIndexUrlError(LayerIndexException):
29 '''Exception raised when unable to access a URL for some reason'''
30 def __init__(self, url, message=""):
31 if message:
32 msg = "Unable to access layerindex url %s: %s" % (url, message)
33 else:
34 msg = "Unable to access layerindex url %s" % url
35 self.url = url
36 LayerIndexException.__init__(self, msg)
37
38class LayerIndexFetchError(LayerIndexException):
39 '''General layerindex fetcher exception when something fails'''
40 def __init__(self, url, message=""):
41 if message:
42 msg = "Unable to fetch layerindex url %s: %s" % (url, message)
43 else:
44 msg = "Unable to fetch layerindex url %s" % url
45 self.url = url
46 LayerIndexException.__init__(self, msg)
47
48
49# Interface to the overall layerindex system
50# the layer may contain one or more individual indexes
51class LayerIndex():
52 def __init__(self, d):
53 if not d:
54 raise LayerIndexException("Must be initialized with bb.data.")
55
56 self.data = d
57
58 # List of LayerIndexObj
59 self.indexes = []
60
61 self.plugins = []
62
63 import bb.utils
64 bb.utils.load_plugins(logger, self.plugins, os.path.dirname(__file__))
65 for plugin in self.plugins:
66 if hasattr(plugin, 'init'):
67 plugin.init(self)
68
69 def __add__(self, other):
70 newIndex = LayerIndex(self.data)
71
72 if self.__class__ != newIndex.__class__ or \
73 other.__class__ != newIndex.__class__:
Andrew Geisslerc9f78652020-09-18 14:11:35 -050074 raise TypeError("Can not add different types.")
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080075
76 for indexEnt in self.indexes:
77 newIndex.indexes.append(indexEnt)
78
79 for indexEnt in other.indexes:
80 newIndex.indexes.append(indexEnt)
81
82 return newIndex
83
84 def _parse_params(self, params):
85 '''Take a parameter list, return a dictionary of parameters.
86
87 Expected to be called from the data of urllib.parse.urlparse(url).params
88
89 If there are two conflicting parameters, last in wins...
90 '''
91
92 param_dict = {}
93 for param in params.split(';'):
94 if not param:
95 continue
96 item = param.split('=', 1)
Andrew Geisslerd1e89492021-02-12 15:35:20 -060097 logger.debug(item)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -080098 param_dict[item[0]] = item[1]
99
100 return param_dict
101
102 def _fetch_url(self, url, username=None, password=None, debuglevel=0):
103 '''Fetch data from a specific URL.
104
105 Fetch something from a specific URL. This is specifically designed to
106 fetch data from a layerindex-web instance, but may be useful for other
107 raw fetch actions.
108
109 It is not designed to be used to fetch recipe sources or similar. the
110 regular fetcher class should used for that.
111
112 It is the responsibility of the caller to check BB_NO_NETWORK and related
113 BB_ALLOWED_NETWORKS.
114 '''
115
116 if not url:
117 raise LayerIndexUrlError(url, "empty url")
118
119 import urllib
120 from urllib.request import urlopen, Request
121 from urllib.parse import urlparse
122
123 up = urlparse(url)
124
125 if username:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600126 logger.debug("Configuring authentication for %s..." % url)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800127 password_mgr = urllib.request.HTTPPasswordMgrWithDefaultRealm()
128 password_mgr.add_password(None, "%s://%s" % (up.scheme, up.netloc), username, password)
129 handler = urllib.request.HTTPBasicAuthHandler(password_mgr)
130 opener = urllib.request.build_opener(handler, urllib.request.HTTPSHandler(debuglevel=debuglevel))
131 else:
132 opener = urllib.request.build_opener(urllib.request.HTTPSHandler(debuglevel=debuglevel))
133
134 urllib.request.install_opener(opener)
135
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600136 logger.debug("Fetching %s (%s)..." % (url, ["without authentication", "with authentication"][bool(username)]))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800137
138 try:
139 res = urlopen(Request(url, headers={'User-Agent': 'Mozilla/5.0 (bitbake/lib/layerindex)'}, unverifiable=True))
140 except urllib.error.HTTPError as e:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600141 logger.debug("HTTP Error: %s: %s" % (e.code, e.reason))
142 logger.debug(" Requested: %s" % (url))
143 logger.debug(" Actual: %s" % (e.geturl()))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800144
145 if e.code == 404:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600146 logger.debug("Request not found.")
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800147 raise LayerIndexFetchError(url, e)
148 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600149 logger.debug("Headers:\n%s" % (e.headers))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800150 raise LayerIndexFetchError(url, e)
151 except OSError as e:
152 error = 0
153 reason = ""
154
155 # Process base OSError first...
156 if hasattr(e, 'errno'):
157 error = e.errno
158 reason = e.strerror
159
160 # Process gaierror (socket error) subclass if available.
161 if hasattr(e, 'reason') and hasattr(e.reason, 'errno') and hasattr(e.reason, 'strerror'):
162 error = e.reason.errno
163 reason = e.reason.strerror
164 if error == -2:
165 raise LayerIndexFetchError(url, "%s: %s" % (e, reason))
166
167 if error and error != 0:
168 raise LayerIndexFetchError(url, "Unexpected exception: [Error %s] %s" % (error, reason))
169 else:
170 raise LayerIndexFetchError(url, "Unable to fetch OSError exception: %s" % e)
171
172 finally:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600173 logger.debug("...fetching %s (%s), done." % (url, ["without authentication", "with authentication"][bool(username)]))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800174
175 return res
176
177
178 def load_layerindex(self, indexURI, load=['layerDependencies', 'recipes', 'machines', 'distros'], reload=False):
179 '''Load the layerindex.
180
181 indexURI - An index to load. (Use multiple calls to load multiple indexes)
182
183 reload - If reload is True, then any previously loaded indexes will be forgotten.
184
185 load - List of elements to load. Default loads all items.
186 Note: plugs may ignore this.
187
188The format of the indexURI:
189
190 <url>;branch=<branch>;cache=<cache>;desc=<description>
191
192 Note: the 'branch' parameter if set can select multiple branches by using
193 comma, such as 'branch=master,morty,pyro'. However, many operations only look
194 at the -first- branch specified!
195
196 The cache value may be undefined, in this case a network failure will
197 result in an error, otherwise the system will look for a file of the cache
198 name and load that instead.
199
200 For example:
201
202 http://layers.openembedded.org/layerindex/api/;branch=master;desc=OpenEmbedded%20Layer%20Index
203 cooker://
204'''
205 if reload:
206 self.indexes = []
207
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600208 logger.debug('Loading: %s' % indexURI)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800209
210 if not self.plugins:
211 raise LayerIndexException("No LayerIndex Plugins available")
212
213 for plugin in self.plugins:
214 # Check if the plugin was initialized
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600215 logger.debug('Trying %s' % plugin.__class__)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800216 if not hasattr(plugin, 'type') or not plugin.type:
217 continue
218 try:
219 # TODO: Implement 'cache', for when the network is not available
220 indexEnt = plugin.load_index(indexURI, load)
221 break
222 except LayerIndexPluginUrlError as e:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600223 logger.debug("%s doesn't support %s" % (plugin.type, e.url))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800224 except NotImplementedError:
225 pass
226 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600227 logger.debug("No plugins support %s" % indexURI)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800228 raise LayerIndexException("No plugins support %s" % indexURI)
229
230 # Mark CONFIG data as something we've added...
231 indexEnt.config['local'] = []
232 indexEnt.config['local'].append('config')
233
234 # No longer permit changes..
235 indexEnt.lockData()
236
237 self.indexes.append(indexEnt)
238
239 def store_layerindex(self, indexURI, index=None):
240 '''Store one layerindex
241
242Typically this will be used to create a local cache file of a remote index.
243
244 file://<path>;branch=<branch>
245
246We can write out in either the restapi or django formats. The split option
247will write out the individual elements split by layer and related components.
248'''
249 if not index:
250 logger.warning('No index to write, nothing to do.')
251 return
252
253 if not self.plugins:
254 raise LayerIndexException("No LayerIndex Plugins available")
255
256 for plugin in self.plugins:
257 # Check if the plugin was initialized
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600258 logger.debug('Trying %s' % plugin.__class__)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800259 if not hasattr(plugin, 'type') or not plugin.type:
260 continue
261 try:
262 plugin.store_index(indexURI, index)
263 break
264 except LayerIndexPluginUrlError as e:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600265 logger.debug("%s doesn't support %s" % (plugin.type, e.url))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800266 except NotImplementedError:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600267 logger.debug("Store not implemented in %s" % plugin.type)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800268 pass
269 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600270 logger.debug("No plugins support %s" % indexURI)
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500271 raise LayerIndexException("No plugins support %s" % indexURI)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800272
273
274 def is_empty(self):
275 '''Return True or False if the index has any usable data.
276
277We check the indexes entries to see if they have a branch set, as well as
278layerBranches set. If not, they are effectively blank.'''
279
280 found = False
281 for index in self.indexes:
282 if index.__bool__():
283 found = True
284 break
285 return not found
286
287
288 def find_vcs_url(self, vcs_url, branch=None):
289 '''Return the first layerBranch with the given vcs_url
290
291 If a branch has not been specified, we will iterate over the branches in
292 the default configuration until the first vcs_url/branch match.'''
293
294 for index in self.indexes:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600295 logger.debug(' searching %s' % index.config['DESCRIPTION'])
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800296 layerBranch = index.find_vcs_url(vcs_url, [branch])
297 if layerBranch:
298 return layerBranch
299 return None
300
301 def find_collection(self, collection, version=None, branch=None):
302 '''Return the first layerBranch with the given collection name
303
304 If a branch has not been specified, we will iterate over the branches in
305 the default configuration until the first collection/branch match.'''
306
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600307 logger.debug('find_collection: %s (%s) %s' % (collection, version, branch))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800308
309 if branch:
310 branches = [branch]
311 else:
312 branches = None
313
314 for index in self.indexes:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600315 logger.debug(' searching %s' % index.config['DESCRIPTION'])
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800316 layerBranch = index.find_collection(collection, version, branches)
317 if layerBranch:
318 return layerBranch
319 else:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600320 logger.debug('Collection %s (%s) not found for branch (%s)' % (collection, version, branch))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800321 return None
322
323 def find_layerbranch(self, name, branch=None):
324 '''Return the layerBranch item for a given name and branch
325
326 If a branch has not been specified, we will iterate over the branches in
327 the default configuration until the first name/branch match.'''
328
329 if branch:
330 branches = [branch]
331 else:
332 branches = None
333
334 for index in self.indexes:
335 layerBranch = index.find_layerbranch(name, branches)
336 if layerBranch:
337 return layerBranch
338 return None
339
340 def find_dependencies(self, names=None, layerbranches=None, ignores=None):
341 '''Return a tuple of all dependencies and valid items for the list of (layer) names
342
343 The dependency scanning happens depth-first. The returned
344 dependencies should be in the best order to define bblayers.
345
346 names - list of layer names (searching layerItems)
347 branches - when specified (with names) only this list of branches are evaluated
348
349 layerbranches - list of layerbranches to resolve dependencies
350
351 ignores - list of layer names to ignore
352
353 return: (dependencies, invalid)
354
355 dependencies[LayerItem.name] = [ LayerBranch, LayerDependency1, LayerDependency2, ... ]
356 invalid = [ LayerItem.name1, LayerItem.name2, ... ]
357 '''
358
359 invalid = []
360
361 # Convert name/branch to layerbranches
362 if layerbranches is None:
363 layerbranches = []
364
365 for name in names:
366 if ignores and name in ignores:
367 continue
368
369 for index in self.indexes:
370 layerbranch = index.find_layerbranch(name)
371 if not layerbranch:
372 # Not in this index, hopefully it's in another...
373 continue
374 layerbranches.append(layerbranch)
375 break
376 else:
377 invalid.append(name)
378
379
Brad Bishop08902b02019-08-20 09:16:51 -0400380 def _resolve_dependencies(layerbranches, ignores, dependencies, invalid, processed=None):
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800381 for layerbranch in layerbranches:
382 if ignores and layerbranch.layer.name in ignores:
383 continue
384
385 # Get a list of dependencies and then recursively process them
386 for layerdependency in layerbranch.index.layerDependencies_layerBranchId[layerbranch.id]:
387 deplayerbranch = layerdependency.dependency_layerBranch
388
389 if ignores and deplayerbranch.layer.name in ignores:
390 continue
391
Brad Bishop08902b02019-08-20 09:16:51 -0400392 # Since this is depth first, we need to know what we're currently processing
393 # in order to avoid infinite recursion on a loop.
394 if processed and deplayerbranch.layer.name in processed:
395 # We have found a recursion...
396 logger.warning('Circular layer dependency found: %s -> %s' % (processed, deplayerbranch.layer.name))
397 continue
398
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800399 # This little block is why we can't re-use the LayerIndexObj version,
400 # we must be able to satisfy each dependencies across layer indexes and
401 # use the layer index order for priority. (r stands for replacement below)
402
403 # If this is the primary index, we can fast path and skip this
404 if deplayerbranch.index != self.indexes[0]:
405 # Is there an entry in a prior index for this collection/version?
406 rdeplayerbranch = self.find_collection(
407 collection=deplayerbranch.collection,
408 version=deplayerbranch.version
409 )
410 if rdeplayerbranch != deplayerbranch:
Andrew Geisslerd1e89492021-02-12 15:35:20 -0600411 logger.debug('Replaced %s:%s:%s with %s:%s:%s' % \
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800412 (deplayerbranch.index.config['DESCRIPTION'],
413 deplayerbranch.branch.name,
414 deplayerbranch.layer.name,
415 rdeplayerbranch.index.config['DESCRIPTION'],
416 rdeplayerbranch.branch.name,
417 rdeplayerbranch.layer.name))
418 deplayerbranch = rdeplayerbranch
419
420 # New dependency, we need to resolve it now... depth-first
421 if deplayerbranch.layer.name not in dependencies:
Brad Bishop08902b02019-08-20 09:16:51 -0400422 # Avoid recursion on this branch.
423 # We copy so we don't end up polluting the depth-first branch with other
424 # branches. Duplication between individual branches IS expected and
425 # handled by 'dependencies' processing.
426 if not processed:
427 local_processed = []
428 else:
429 local_processed = processed.copy()
430 local_processed.append(deplayerbranch.layer.name)
431
432 (dependencies, invalid) = _resolve_dependencies([deplayerbranch], ignores, dependencies, invalid, local_processed)
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800433
434 if deplayerbranch.layer.name not in dependencies:
435 dependencies[deplayerbranch.layer.name] = [deplayerbranch, layerdependency]
436 else:
437 if layerdependency not in dependencies[deplayerbranch.layer.name]:
438 dependencies[deplayerbranch.layer.name].append(layerdependency)
439
440 return (dependencies, invalid)
441
442 # OK, resolve this one...
443 dependencies = OrderedDict()
444 (dependencies, invalid) = _resolve_dependencies(layerbranches, ignores, dependencies, invalid)
445
446 for layerbranch in layerbranches:
447 if layerbranch.layer.name not in dependencies:
448 dependencies[layerbranch.layer.name] = [layerbranch]
449
450 return (dependencies, invalid)
451
452
453 def list_obj(self, object):
454 '''Print via the plain logger object information
455
456This function is used to implement debugging and provide the user info.
457'''
458 for lix in self.indexes:
459 if not hasattr(lix, object):
460 continue
461
462 logger.plain ('')
463 logger.plain ('Index: %s' % lix.config['DESCRIPTION'])
464
465 output = []
466
467 if object == 'branches':
468 logger.plain ('%s %s %s' % ('{:26}'.format('branch'), '{:34}'.format('description'), '{:22}'.format('bitbake branch')))
469 logger.plain ('{:-^80}'.format(""))
470 for branchid in lix.branches:
471 output.append('%s %s %s' % (
472 '{:26}'.format(lix.branches[branchid].name),
473 '{:34}'.format(lix.branches[branchid].short_description),
474 '{:22}'.format(lix.branches[branchid].bitbake_branch)
475 ))
476 for line in sorted(output):
477 logger.plain (line)
478
479 continue
480
481 if object == 'layerItems':
482 logger.plain ('%s %s' % ('{:26}'.format('layer'), '{:34}'.format('description')))
483 logger.plain ('{:-^80}'.format(""))
484 for layerid in lix.layerItems:
485 output.append('%s %s' % (
486 '{:26}'.format(lix.layerItems[layerid].name),
487 '{:34}'.format(lix.layerItems[layerid].summary)
488 ))
489 for line in sorted(output):
490 logger.plain (line)
491
492 continue
493
494 if object == 'layerBranches':
495 logger.plain ('%s %s %s' % ('{:26}'.format('layer'), '{:34}'.format('description'), '{:19}'.format('collection:version')))
496 logger.plain ('{:-^80}'.format(""))
497 for layerbranchid in lix.layerBranches:
498 output.append('%s %s %s' % (
499 '{:26}'.format(lix.layerBranches[layerbranchid].layer.name),
500 '{:34}'.format(lix.layerBranches[layerbranchid].layer.summary),
501 '{:19}'.format("%s:%s" %
502 (lix.layerBranches[layerbranchid].collection,
503 lix.layerBranches[layerbranchid].version)
504 )
505 ))
506 for line in sorted(output):
507 logger.plain (line)
508
509 continue
510
511 if object == 'layerDependencies':
512 logger.plain ('%s %s %s %s' % ('{:19}'.format('branch'), '{:26}'.format('layer'), '{:11}'.format('dependency'), '{:26}'.format('layer')))
513 logger.plain ('{:-^80}'.format(""))
514 for layerDependency in lix.layerDependencies:
515 if not lix.layerDependencies[layerDependency].dependency_layerBranch:
516 continue
517
518 output.append('%s %s %s %s' % (
519 '{:19}'.format(lix.layerDependencies[layerDependency].layerbranch.branch.name),
520 '{:26}'.format(lix.layerDependencies[layerDependency].layerbranch.layer.name),
521 '{:11}'.format('requires' if lix.layerDependencies[layerDependency].required else 'recommends'),
522 '{:26}'.format(lix.layerDependencies[layerDependency].dependency_layerBranch.layer.name)
523 ))
524 for line in sorted(output):
525 logger.plain (line)
526
527 continue
528
529 if object == 'recipes':
530 logger.plain ('%s %s %s' % ('{:20}'.format('recipe'), '{:10}'.format('version'), 'layer'))
531 logger.plain ('{:-^80}'.format(""))
532 output = []
533 for recipe in lix.recipes:
534 output.append('%s %s %s' % (
535 '{:30}'.format(lix.recipes[recipe].pn),
536 '{:30}'.format(lix.recipes[recipe].pv),
537 lix.recipes[recipe].layer.name
538 ))
539 for line in sorted(output):
540 logger.plain (line)
541
542 continue
543
544 if object == 'machines':
545 logger.plain ('%s %s %s' % ('{:24}'.format('machine'), '{:34}'.format('description'), '{:19}'.format('layer')))
546 logger.plain ('{:-^80}'.format(""))
547 for machine in lix.machines:
548 output.append('%s %s %s' % (
549 '{:24}'.format(lix.machines[machine].name),
550 '{:34}'.format(lix.machines[machine].description)[:34],
551 '{:19}'.format(lix.machines[machine].layerbranch.layer.name)
552 ))
553 for line in sorted(output):
554 logger.plain (line)
555
556 continue
557
558 if object == 'distros':
559 logger.plain ('%s %s %s' % ('{:24}'.format('distro'), '{:34}'.format('description'), '{:19}'.format('layer')))
560 logger.plain ('{:-^80}'.format(""))
561 for distro in lix.distros:
562 output.append('%s %s %s' % (
563 '{:24}'.format(lix.distros[distro].name),
564 '{:34}'.format(lix.distros[distro].description)[:34],
565 '{:19}'.format(lix.distros[distro].layerbranch.layer.name)
566 ))
567 for line in sorted(output):
568 logger.plain (line)
569
570 continue
571
572 logger.plain ('')
573
574
575# This class holds a single layer index instance
576# The LayerIndexObj is made up of dictionary of elements, such as:
577# index['config'] - configuration data for this index
578# index['branches'] - dictionary of Branch objects, by id number
579# index['layerItems'] - dictionary of layerItem objects, by id number
580# ...etc... (See: http://layers.openembedded.org/layerindex/api/)
581#
582# The class needs to manage the 'index' entries and allow easily adding
583# of new items, as well as simply loading of the items.
584class LayerIndexObj():
585 def __init__(self):
586 super().__setattr__('_index', {})
587 super().__setattr__('_lock', False)
588
589 def __bool__(self):
590 '''False if the index is effectively empty
591
592 We check the index to see if it has a branch set, as well as
593 layerbranches set. If not, it is effectively blank.'''
594
595 if not bool(self._index):
596 return False
597
598 try:
599 if self.branches and self.layerBranches:
600 return True
601 except AttributeError:
602 pass
603
604 return False
605
606 def __getattr__(self, name):
607 if name.startswith('_'):
608 return super().__getattribute__(name)
609
610 if name not in self._index:
611 raise AttributeError('%s not in index datastore' % name)
612
613 return self._index[name]
614
615 def __setattr__(self, name, value):
616 if self.isLocked():
617 raise TypeError("Can not set attribute '%s': index is locked" % name)
618
619 if name.startswith('_'):
620 super().__setattr__(name, value)
621 return
622
623 self._index[name] = value
624
625 def __delattr__(self, name):
626 if self.isLocked():
627 raise TypeError("Can not delete attribute '%s': index is locked" % name)
628
629 if name.startswith('_'):
630 super().__delattr__(name)
631
632 self._index.pop(name)
633
634 def lockData(self):
635 '''Lock data object (make it readonly)'''
636 super().__setattr__("_lock", True)
637
638 def unlockData(self):
639 '''unlock data object (make it readonly)'''
640 super().__setattr__("_lock", False)
641
642 # When the data is unlocked, we have to clear the caches, as
643 # modification is allowed!
644 del(self._layerBranches_layerId_branchId)
645 del(self._layerDependencies_layerBranchId)
646 del(self._layerBranches_vcsUrl)
647
648 def isLocked(self):
649 '''Is this object locked (readonly)?'''
650 return self._lock
651
652 def add_element(self, indexname, objs):
653 '''Add a layer index object to index.<indexname>'''
654 if indexname not in self._index:
655 self._index[indexname] = {}
656
657 for obj in objs:
658 if obj.id in self._index[indexname]:
659 if self._index[indexname][obj.id] == obj:
660 continue
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500661 raise LayerIndexException('Conflict adding object %s(%s) to index' % (indexname, obj.id))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800662 self._index[indexname][obj.id] = obj
663
664 def add_raw_element(self, indexname, objtype, rawobjs):
665 '''Convert a raw layer index data item to a layer index item object and add to the index'''
666 objs = []
667 for entry in rawobjs:
668 objs.append(objtype(self, entry))
669 self.add_element(indexname, objs)
670
671 # Quick lookup table for searching layerId and branchID combos
672 @property
673 def layerBranches_layerId_branchId(self):
674 def createCache(self):
675 cache = {}
676 for layerbranchid in self.layerBranches:
677 layerbranch = self.layerBranches[layerbranchid]
678 cache["%s:%s" % (layerbranch.layer_id, layerbranch.branch_id)] = layerbranch
679 return cache
680
681 if self.isLocked():
682 cache = getattr(self, '_layerBranches_layerId_branchId', None)
683 else:
684 cache = None
685
686 if not cache:
687 cache = createCache(self)
688
689 if self.isLocked():
690 super().__setattr__('_layerBranches_layerId_branchId', cache)
691
692 return cache
693
694 # Quick lookup table for finding all dependencies of a layerBranch
695 @property
696 def layerDependencies_layerBranchId(self):
697 def createCache(self):
698 cache = {}
699 # This ensures empty lists for all branchids
700 for layerbranchid in self.layerBranches:
701 cache[layerbranchid] = []
702
703 for layerdependencyid in self.layerDependencies:
704 layerdependency = self.layerDependencies[layerdependencyid]
705 cache[layerdependency.layerbranch_id].append(layerdependency)
706 return cache
707
708 if self.isLocked():
709 cache = getattr(self, '_layerDependencies_layerBranchId', None)
710 else:
711 cache = None
712
713 if not cache:
714 cache = createCache(self)
715
716 if self.isLocked():
717 super().__setattr__('_layerDependencies_layerBranchId', cache)
718
719 return cache
720
721 # Quick lookup table for finding all instances of a vcs_url
722 @property
723 def layerBranches_vcsUrl(self):
724 def createCache(self):
725 cache = {}
726 for layerbranchid in self.layerBranches:
727 layerbranch = self.layerBranches[layerbranchid]
728 if layerbranch.layer.vcs_url not in cache:
729 cache[layerbranch.layer.vcs_url] = [layerbranch]
730 else:
731 cache[layerbranch.layer.vcs_url].append(layerbranch)
732 return cache
733
734 if self.isLocked():
735 cache = getattr(self, '_layerBranches_vcsUrl', None)
736 else:
737 cache = None
738
739 if not cache:
740 cache = createCache(self)
741
742 if self.isLocked():
743 super().__setattr__('_layerBranches_vcsUrl', cache)
744
745 return cache
746
747
748 def find_vcs_url(self, vcs_url, branches=None):
749 ''''Return the first layerBranch with the given vcs_url
750
751 If a list of branches has not been specified, we will iterate on
752 all branches until the first vcs_url is found.'''
753
754 if not self.__bool__():
755 return None
756
757 for layerbranch in self.layerBranches_vcsUrl:
758 if branches and layerbranch.branch.name not in branches:
759 continue
760
761 return layerbranch
762
763 return None
764
765
766 def find_collection(self, collection, version=None, branches=None):
767 '''Return the first layerBranch with the given collection name
768
769 If a list of branches has not been specified, we will iterate on
770 all branches until the first collection is found.'''
771
772 if not self.__bool__():
773 return None
774
775 for layerbranchid in self.layerBranches:
776 layerbranch = self.layerBranches[layerbranchid]
777 if branches and layerbranch.branch.name not in branches:
778 continue
779
780 if layerbranch.collection == collection and \
781 (version is None or version == layerbranch.version):
782 return layerbranch
783
784 return None
785
786
787 def find_layerbranch(self, name, branches=None):
788 '''Return the first layerbranch whose layer name matches
789
790 If a list of branches has not been specified, we will iterate on
791 all branches until the first layer with that name is found.'''
792
793 if not self.__bool__():
794 return None
795
796 for layerbranchid in self.layerBranches:
797 layerbranch = self.layerBranches[layerbranchid]
798 if branches and layerbranch.branch.name not in branches:
799 continue
800
801 if layerbranch.layer.name == name:
802 return layerbranch
803
804 return None
805
806 def find_dependencies(self, names=None, branches=None, layerBranches=None, ignores=None):
807 '''Return a tuple of all dependencies and valid items for the list of (layer) names
808
809 The dependency scanning happens depth-first. The returned
810 dependencies should be in the best order to define bblayers.
811
812 names - list of layer names (searching layerItems)
813 branches - when specified (with names) only this list of branches are evaluated
814
815 layerBranches - list of layerBranches to resolve dependencies
816
817 ignores - list of layer names to ignore
818
819 return: (dependencies, invalid)
820
821 dependencies[LayerItem.name] = [ LayerBranch, LayerDependency1, LayerDependency2, ... ]
822 invalid = [ LayerItem.name1, LayerItem.name2, ... ]'''
823
824 invalid = []
825
826 # Convert name/branch to layerBranches
827 if layerbranches is None:
828 layerbranches = []
829
830 for name in names:
831 if ignores and name in ignores:
832 continue
833
834 layerbranch = self.find_layerbranch(name, branches)
835 if not layerbranch:
836 invalid.append(name)
837 else:
838 layerbranches.append(layerbranch)
839
840 for layerbranch in layerbranches:
841 if layerbranch.index != self:
842 raise LayerIndexException("Can not resolve dependencies across indexes with this class function!")
843
844 def _resolve_dependencies(layerbranches, ignores, dependencies, invalid):
845 for layerbranch in layerbranches:
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500846 if ignores and layerbranch.layer.name in ignores:
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800847 continue
848
Andrew Geisslerc9f78652020-09-18 14:11:35 -0500849 for layerdependency in layerbranch.index.layerDependencies_layerBranchId[layerbranch.id]:
850 deplayerbranch = layerdependency.dependency_layerBranch
Brad Bishop1a4b7ee2018-12-16 17:11:34 -0800851
852 if ignores and deplayerbranch.layer.name in ignores:
853 continue
854
855 # New dependency, we need to resolve it now... depth-first
856 if deplayerbranch.layer.name not in dependencies:
857 (dependencies, invalid) = _resolve_dependencies([deplayerbranch], ignores, dependencies, invalid)
858
859 if deplayerbranch.layer.name not in dependencies:
860 dependencies[deplayerbranch.layer.name] = [deplayerbranch, layerdependency]
861 else:
862 if layerdependency not in dependencies[deplayerbranch.layer.name]:
863 dependencies[deplayerbranch.layer.name].append(layerdependency)
864
865 return (dependencies, invalid)
866
867 # OK, resolve this one...
868 dependencies = OrderedDict()
869 (dependencies, invalid) = _resolve_dependencies(layerbranches, ignores, dependencies, invalid)
870
871 # Is this item already in the list, if not add it
872 for layerbranch in layerbranches:
873 if layerbranch.layer.name not in dependencies:
874 dependencies[layerbranch.layer.name] = [layerbranch]
875
876 return (dependencies, invalid)
877
878
879# Define a basic LayerIndexItemObj. This object forms the basis for all other
880# objects. The raw Layer Index data is stored in the _data element, but we
881# do not want users to access data directly. So wrap this and protect it
882# from direct manipulation.
883#
884# It is up to the insantiators of the objects to fill them out, and once done
885# lock the objects to prevent further accidently manipulation.
886#
887# Using the getattr, setattr and properties we can access and manipulate
888# the data within the data element.
889class LayerIndexItemObj():
890 def __init__(self, index, data=None, lock=False):
891 if data is None:
892 data = {}
893
894 if type(data) != type(dict()):
895 raise TypeError('data (%s) is not a dict' % type(data))
896
897 super().__setattr__('_lock', lock)
898 super().__setattr__('index', index)
899 super().__setattr__('_data', data)
900
901 def __eq__(self, other):
902 if self.__class__ != other.__class__:
903 return False
904 res=(self._data == other._data)
905 return res
906
907 def __bool__(self):
908 return bool(self._data)
909
910 def __getattr__(self, name):
911 # These are internal to THIS class, and not part of data
912 if name == "index" or name.startswith('_'):
913 return super().__getattribute__(name)
914
915 if name not in self._data:
916 raise AttributeError('%s not in datastore' % name)
917
918 return self._data[name]
919
920 def _setattr(self, name, value, prop=True):
921 '''__setattr__ like function, but with control over property object behavior'''
922 if self.isLocked():
923 raise TypeError("Can not set attribute '%s': Object data is locked" % name)
924
925 if name.startswith('_'):
926 super().__setattr__(name, value)
927 return
928
929 # Since __setattr__ runs before properties, we need to check if
930 # there is a setter property and then execute it
931 # ... or return self._data[name]
932 propertyobj = getattr(self.__class__, name, None)
933 if prop and isinstance(propertyobj, property):
934 if propertyobj.fset:
935 propertyobj.fset(self, value)
936 else:
937 raise AttributeError('Attribute %s is readonly, and may not be set' % name)
938 else:
939 self._data[name] = value
940
941 def __setattr__(self, name, value):
942 self._setattr(name, value, prop=True)
943
944 def _delattr(self, name, prop=True):
945 # Since __delattr__ runs before properties, we need to check if
946 # there is a deleter property and then execute it
947 # ... or we pop it ourselves..
948 propertyobj = getattr(self.__class__, name, None)
949 if prop and isinstance(propertyobj, property):
950 if propertyobj.fdel:
951 propertyobj.fdel(self)
952 else:
953 raise AttributeError('Attribute %s is readonly, and may not be deleted' % name)
954 else:
955 self._data.pop(name)
956
957 def __delattr__(self, name):
958 self._delattr(name, prop=True)
959
960 def lockData(self):
961 '''Lock data object (make it readonly)'''
962 super().__setattr__("_lock", True)
963
964 def unlockData(self):
965 '''unlock data object (make it readonly)'''
966 super().__setattr__("_lock", False)
967
968 def isLocked(self):
969 '''Is this object locked (readonly)?'''
970 return self._lock
971
972# Branch object
973class Branch(LayerIndexItemObj):
974 def define_data(self, id, name, bitbake_branch,
975 short_description=None, sort_priority=1,
976 updates_enabled=True, updated=None,
977 update_environment=None):
978 self.id = id
979 self.name = name
980 self.bitbake_branch = bitbake_branch
981 self.short_description = short_description or name
982 self.sort_priority = sort_priority
983 self.updates_enabled = updates_enabled
984 self.updated = updated or datetime.datetime.today().isoformat()
985 self.update_environment = update_environment
986
987 @property
988 def name(self):
989 return self.__getattr__('name')
990
991 @name.setter
992 def name(self, value):
993 self._data['name'] = value
994
995 if self.bitbake_branch == value:
996 self.bitbake_branch = ""
997
998 @name.deleter
999 def name(self):
1000 self._delattr('name', prop=False)
1001
1002 @property
1003 def bitbake_branch(self):
1004 try:
1005 return self.__getattr__('bitbake_branch')
1006 except AttributeError:
1007 return self.name
1008
1009 @bitbake_branch.setter
1010 def bitbake_branch(self, value):
1011 if self.name == value:
1012 self._data['bitbake_branch'] = ""
1013 else:
1014 self._data['bitbake_branch'] = value
1015
1016 @bitbake_branch.deleter
1017 def bitbake_branch(self):
1018 self._delattr('bitbake_branch', prop=False)
1019
1020
1021class LayerItem(LayerIndexItemObj):
1022 def define_data(self, id, name, status='P',
1023 layer_type='A', summary=None,
1024 description=None,
1025 vcs_url=None, vcs_web_url=None,
1026 vcs_web_tree_base_url=None,
1027 vcs_web_file_base_url=None,
1028 usage_url=None,
1029 mailing_list_url=None,
1030 index_preference=1,
1031 classic=False,
1032 updated=None):
1033 self.id = id
1034 self.name = name
1035 self.status = status
1036 self.layer_type = layer_type
1037 self.summary = summary or name
1038 self.description = description or summary or name
1039 self.vcs_url = vcs_url
1040 self.vcs_web_url = vcs_web_url
1041 self.vcs_web_tree_base_url = vcs_web_tree_base_url
1042 self.vcs_web_file_base_url = vcs_web_file_base_url
1043 self.index_preference = index_preference
1044 self.classic = classic
1045 self.updated = updated or datetime.datetime.today().isoformat()
1046
1047
1048class LayerBranch(LayerIndexItemObj):
1049 def define_data(self, id, collection, version, layer, branch,
1050 vcs_subdir="", vcs_last_fetch=None,
1051 vcs_last_rev=None, vcs_last_commit=None,
1052 actual_branch="",
1053 updated=None):
1054 self.id = id
1055 self.collection = collection
1056 self.version = version
1057 if isinstance(layer, LayerItem):
1058 self.layer = layer
1059 else:
1060 self.layer_id = layer
1061
1062 if isinstance(branch, Branch):
1063 self.branch = branch
1064 else:
1065 self.branch_id = branch
1066
1067 self.vcs_subdir = vcs_subdir
1068 self.vcs_last_fetch = vcs_last_fetch
1069 self.vcs_last_rev = vcs_last_rev
1070 self.vcs_last_commit = vcs_last_commit
1071 self.actual_branch = actual_branch
1072 self.updated = updated or datetime.datetime.today().isoformat()
1073
1074 # This is a little odd, the _data attribute is 'layer', but it's really
1075 # referring to the layer id.. so lets adjust this to make it useful
1076 @property
1077 def layer_id(self):
1078 return self.__getattr__('layer')
1079
1080 @layer_id.setter
1081 def layer_id(self, value):
1082 self._setattr('layer', value, prop=False)
1083
1084 @layer_id.deleter
1085 def layer_id(self):
1086 self._delattr('layer', prop=False)
1087
1088 @property
1089 def layer(self):
1090 try:
1091 return self.index.layerItems[self.layer_id]
1092 except KeyError:
1093 raise AttributeError('Unable to find layerItems in index to map layer_id %s' % self.layer_id)
1094 except IndexError:
1095 raise AttributeError('Unable to find layer_id %s in index layerItems' % self.layer_id)
1096
1097 @layer.setter
1098 def layer(self, value):
1099 if not isinstance(value, LayerItem):
1100 raise TypeError('value is not a LayerItem')
1101 if self.index != value.index:
1102 raise AttributeError('Object and value do not share the same index and thus key set.')
1103 self.layer_id = value.id
1104
1105 @layer.deleter
1106 def layer(self):
1107 del self.layer_id
1108
1109 @property
1110 def branch_id(self):
1111 return self.__getattr__('branch')
1112
1113 @branch_id.setter
1114 def branch_id(self, value):
1115 self._setattr('branch', value, prop=False)
1116
1117 @branch_id.deleter
1118 def branch_id(self):
1119 self._delattr('branch', prop=False)
1120
1121 @property
1122 def branch(self):
1123 try:
Andrew Geisslerd1e89492021-02-12 15:35:20 -06001124 logger.debug("Get branch object from branches[%s]" % (self.branch_id))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001125 return self.index.branches[self.branch_id]
1126 except KeyError:
1127 raise AttributeError('Unable to find branches in index to map branch_id %s' % self.branch_id)
1128 except IndexError:
1129 raise AttributeError('Unable to find branch_id %s in index branches' % self.branch_id)
1130
1131 @branch.setter
1132 def branch(self, value):
1133 if not isinstance(value, LayerItem):
1134 raise TypeError('value is not a LayerItem')
1135 if self.index != value.index:
1136 raise AttributeError('Object and value do not share the same index and thus key set.')
1137 self.branch_id = value.id
1138
1139 @branch.deleter
1140 def branch(self):
1141 del self.branch_id
1142
1143 @property
1144 def actual_branch(self):
1145 if self.__getattr__('actual_branch'):
1146 return self.__getattr__('actual_branch')
1147 else:
1148 return self.branch.name
1149
1150 @actual_branch.setter
1151 def actual_branch(self, value):
Andrew Geisslerd1e89492021-02-12 15:35:20 -06001152 logger.debug("Set actual_branch to %s .. name is %s" % (value, self.branch.name))
Brad Bishop1a4b7ee2018-12-16 17:11:34 -08001153 if value != self.branch.name:
1154 self._setattr('actual_branch', value, prop=False)
1155 else:
1156 self._setattr('actual_branch', '', prop=False)
1157
1158 @actual_branch.deleter
1159 def actual_branch(self):
1160 self._delattr('actual_branch', prop=False)
1161
1162# Extend LayerIndexItemObj with common LayerBranch manipulations
1163# All of the remaining LayerIndex objects refer to layerbranch, and it is
1164# up to the user to follow that back through the LayerBranch object into
1165# the layer object to get various attributes. So add an intermediate set
1166# of attributes that can easily get us the layerbranch as well as layer.
1167
1168class LayerIndexItemObj_LayerBranch(LayerIndexItemObj):
1169 @property
1170 def layerbranch_id(self):
1171 return self.__getattr__('layerbranch')
1172
1173 @layerbranch_id.setter
1174 def layerbranch_id(self, value):
1175 self._setattr('layerbranch', value, prop=False)
1176
1177 @layerbranch_id.deleter
1178 def layerbranch_id(self):
1179 self._delattr('layerbranch', prop=False)
1180
1181 @property
1182 def layerbranch(self):
1183 try:
1184 return self.index.layerBranches[self.layerbranch_id]
1185 except KeyError:
1186 raise AttributeError('Unable to find layerBranches in index to map layerbranch_id %s' % self.layerbranch_id)
1187 except IndexError:
1188 raise AttributeError('Unable to find layerbranch_id %s in index branches' % self.layerbranch_id)
1189
1190 @layerbranch.setter
1191 def layerbranch(self, value):
1192 if not isinstance(value, LayerBranch):
1193 raise TypeError('value (%s) is not a layerBranch' % type(value))
1194 if self.index != value.index:
1195 raise AttributeError('Object and value do not share the same index and thus key set.')
1196 self.layerbranch_id = value.id
1197
1198 @layerbranch.deleter
1199 def layerbranch(self):
1200 del self.layerbranch_id
1201
1202 @property
1203 def layer_id(self):
1204 return self.layerbranch.layer_id
1205
1206 # Doesn't make sense to set or delete layer_id
1207
1208 @property
1209 def layer(self):
1210 return self.layerbranch.layer
1211
1212 # Doesn't make sense to set or delete layer
1213
1214
1215class LayerDependency(LayerIndexItemObj_LayerBranch):
1216 def define_data(self, id, layerbranch, dependency, required=True):
1217 self.id = id
1218 if isinstance(layerbranch, LayerBranch):
1219 self.layerbranch = layerbranch
1220 else:
1221 self.layerbranch_id = layerbranch
1222 if isinstance(dependency, LayerDependency):
1223 self.dependency = dependency
1224 else:
1225 self.dependency_id = dependency
1226 self.required = required
1227
1228 @property
1229 def dependency_id(self):
1230 return self.__getattr__('dependency')
1231
1232 @dependency_id.setter
1233 def dependency_id(self, value):
1234 self._setattr('dependency', value, prop=False)
1235
1236 @dependency_id.deleter
1237 def dependency_id(self):
1238 self._delattr('dependency', prop=False)
1239
1240 @property
1241 def dependency(self):
1242 try:
1243 return self.index.layerItems[self.dependency_id]
1244 except KeyError:
1245 raise AttributeError('Unable to find layerItems in index to map layerbranch_id %s' % self.dependency_id)
1246 except IndexError:
1247 raise AttributeError('Unable to find dependency_id %s in index layerItems' % self.dependency_id)
1248
1249 @dependency.setter
1250 def dependency(self, value):
1251 if not isinstance(value, LayerDependency):
1252 raise TypeError('value (%s) is not a dependency' % type(value))
1253 if self.index != value.index:
1254 raise AttributeError('Object and value do not share the same index and thus key set.')
1255 self.dependency_id = value.id
1256
1257 @dependency.deleter
1258 def dependency(self):
1259 self._delattr('dependency', prop=False)
1260
1261 @property
1262 def dependency_layerBranch(self):
1263 layerid = self.dependency_id
1264 branchid = self.layerbranch.branch_id
1265
1266 try:
1267 return self.index.layerBranches_layerId_branchId["%s:%s" % (layerid, branchid)]
1268 except IndexError:
1269 # layerBranches_layerId_branchId -- but not layerId:branchId
1270 raise AttributeError('Unable to find layerId:branchId %s:%s in index layerBranches_layerId_branchId' % (layerid, branchid))
1271 except KeyError:
1272 raise AttributeError('Unable to find layerId:branchId %s:%s in layerItems and layerBranches' % (layerid, branchid))
1273
1274 # dependency_layerBranch doesn't make sense to set or del
1275
1276
1277class Recipe(LayerIndexItemObj_LayerBranch):
1278 def define_data(self, id,
1279 filename, filepath, pn, pv, layerbranch,
1280 summary="", description="", section="", license="",
1281 homepage="", bugtracker="", provides="", bbclassextend="",
1282 inherits="", blacklisted="", updated=None):
1283 self.id = id
1284 self.filename = filename
1285 self.filepath = filepath
1286 self.pn = pn
1287 self.pv = pv
1288 self.summary = summary
1289 self.description = description
1290 self.section = section
1291 self.license = license
1292 self.homepage = homepage
1293 self.bugtracker = bugtracker
1294 self.provides = provides
1295 self.bbclassextend = bbclassextend
1296 self.inherits = inherits
1297 self.updated = updated or datetime.datetime.today().isoformat()
1298 self.blacklisted = blacklisted
1299 if isinstance(layerbranch, LayerBranch):
1300 self.layerbranch = layerbranch
1301 else:
1302 self.layerbranch_id = layerbranch
1303
1304 @property
1305 def fullpath(self):
1306 return os.path.join(self.filepath, self.filename)
1307
1308 # Set would need to understand how to split it
1309 # del would we del both parts?
1310
1311 @property
1312 def inherits(self):
1313 if 'inherits' not in self._data:
1314 # Older indexes may not have this, so emulate it
1315 if '-image-' in self.pn:
1316 return 'image'
1317 return self.__getattr__('inherits')
1318
1319 @inherits.setter
1320 def inherits(self, value):
1321 return self._setattr('inherits', value, prop=False)
1322
1323 @inherits.deleter
1324 def inherits(self):
1325 return self._delattr('inherits', prop=False)
1326
1327
1328class Machine(LayerIndexItemObj_LayerBranch):
1329 def define_data(self, id,
1330 name, description, layerbranch,
1331 updated=None):
1332 self.id = id
1333 self.name = name
1334 self.description = description
1335 if isinstance(layerbranch, LayerBranch):
1336 self.layerbranch = layerbranch
1337 else:
1338 self.layerbranch_id = layerbranch
1339 self.updated = updated or datetime.datetime.today().isoformat()
1340
1341class Distro(LayerIndexItemObj_LayerBranch):
1342 def define_data(self, id,
1343 name, description, layerbranch,
1344 updated=None):
1345 self.id = id
1346 self.name = name
1347 self.description = description
1348 if isinstance(layerbranch, LayerBranch):
1349 self.layerbranch = layerbranch
1350 else:
1351 self.layerbranch_id = layerbranch
1352 self.updated = updated or datetime.datetime.today().isoformat()
1353
1354# When performing certain actions, we may need to sort the data.
1355# This will allow us to keep it consistent from run to run.
1356def sort_entry(item):
1357 newitem = item
1358 try:
1359 if type(newitem) == type(dict()):
1360 newitem = OrderedDict(sorted(newitem.items(), key=lambda t: t[0]))
1361 for index in newitem:
1362 newitem[index] = sort_entry(newitem[index])
1363 elif type(newitem) == type(list()):
1364 newitem.sort(key=lambda obj: obj['id'])
1365 for index, _ in enumerate(newitem):
1366 newitem[index] = sort_entry(newitem[index])
1367 except:
1368 logger.error('Sort failed for item %s' % type(item))
1369 pass
1370
1371 return newitem