summaryrefslogtreecommitdiff
path: root/morphlib/morphset.py
diff options
context:
space:
mode:
Diffstat (limited to 'morphlib/morphset.py')
-rw-r--r--morphlib/morphset.py247
1 files changed, 247 insertions, 0 deletions
diff --git a/morphlib/morphset.py b/morphlib/morphset.py
new file mode 100644
index 00000000..bf061f94
--- /dev/null
+++ b/morphlib/morphset.py
@@ -0,0 +1,247 @@
+# Copyright (C) 2013-2014 Codethink Limited
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation; version 2 of the License.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License along
+# with this program; if not, write to the Free Software Foundation, Inc.,
+# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+#
+# =*= License: GPL-2 =*=
+
+
+import morphlib
+
+
+class ChunkNotInStratumError(morphlib.Error):
+
+ def __init__(self, stratum_name, chunk_name):
+ self.msg = (
+ 'Stratum %s does not contain %s' % (stratum_name, chunk_name))
+
+
+class MorphologySet(object):
+
+ '''Store and manipulate a set of Morphology objects.'''
+
+ def __init__(self):
+ self.morphologies = []
+
+ def add_morphology(self, morphology):
+ '''Add a morphology object to the set, unless it's there already.'''
+
+ triplet = (
+ morphology.repo_url,
+ morphology.ref,
+ morphology.filename
+ )
+ for existing in self.morphologies:
+ existing_triplet = (
+ existing.repo_url,
+ existing.ref,
+ existing.filename
+ )
+ if existing_triplet == triplet:
+ return
+
+ self.morphologies.append(morphology)
+
+ def has(self, repo_url, ref, filename):
+ '''Does the set have a morphology for the given triplet?'''
+ return self._get_morphology(repo_url, ref, filename) is not None
+
+ def _get_morphology(self, repo_url, ref, filename):
+ for m in self.morphologies:
+ if (m.repo_url == repo_url and
+ m.ref == ref and
+ m.filename == filename):
+ return m
+ return None
+
+ def _find_spec(self, specs, wanted_name):
+ for spec in specs:
+ name = spec.get('name', spec.get('morph'))
+ if name == wanted_name:
+ return spec.get('repo'), spec.get('ref'), name
+ return None, None, None
+
+ def get_chunk_triplet(self, stratum_morph, chunk_name):
+ '''Return the repo url, ref, morph name triplet for a chunk.
+
+ Given a stratum morphology, find the triplet used to refer to
+ a given chunk. Note that because of how the chunk may be
+ referred to using either name or morph fields in the morphology,
+ the morph field (or its computed value) is always returned.
+ Note also that the morph field, not the filename, is returned.
+
+ Raise ChunkNotInStratumError if the chunk is not found in the
+ stratum.
+
+ '''
+
+ repo_url, ref, morph = self._find_spec(
+ stratum_morph['chunks'], chunk_name)
+ if (repo_url, ref, morph) == (None, None, None):
+ raise ChunkNotInStratumError(stratum_morph['name'], chunk_name)
+ return repo_url, ref, morph
+
+ def traverse_specs(self, cb_process, cb_filter=lambda s: True):
+ '''Higher-order function for processing every spec.
+
+ This traverses every spec in all the morphologies, so all chunk,
+ stratum and stratum-build-depend specs are visited.
+
+ It is to be passed one or two callbacks. `cb_process` is given
+ a spec, which it may alter, but if it does, it must return True.
+
+ `cb_filter` is given the morphology, the kind of spec it is
+ working on in addition to the spec itself.
+
+ `cb_filter` is expected to decide whether to run `cb_process`
+ on the spec.
+
+ Arguably this could be checked in `cb_process`, but it can be less
+ logic over all since `cb_process` need not conditionally return.
+
+ If any specs have been altered, at the end of iteration, any
+ morphologies in the MorphologySet that are referred to by an
+ altered spec are also changed.
+
+ This requires a full iteration of the MorphologySet, so it is not a
+ cheap operation.
+
+ A coroutine was attempted, but it required the same amount of
+ code at the call site as doing it by hand.
+
+ '''
+
+ altered_references = {}
+
+ def process_spec_list(m, kind):
+ specs = m[kind]
+ for spec in specs:
+ if cb_filter(m, kind, spec):
+ fn = morphlib.util.sanitise_morphology_path(
+ spec['morph'] if 'morph' in spec else spec['name'])
+ orig_spec = (spec.get('repo'), spec.get('ref'), fn)
+ dirtied = cb_process(m, kind, spec)
+ if dirtied:
+ m.dirty = True
+ altered_references[orig_spec] = spec
+
+ for m in self.morphologies:
+ if m['kind'] == 'system':
+ process_spec_list(m, 'strata')
+ elif m['kind'] == 'stratum':
+ process_spec_list(m, 'build-depends')
+ process_spec_list(m, 'chunks')
+
+ for m in self.morphologies:
+ tup = (m.repo_url, m.ref, m.filename)
+ if tup in altered_references:
+ spec = altered_references[tup]
+ if m.ref != spec.get('ref'):
+ m.ref = spec.get('ref')
+ m.dirty = True
+ file = morphlib.util.sanitise_morphology_path(
+ spec['morph'] if 'morph' in spec else spec['name'])
+ assert (m.filename == file
+ or m.repo_url == spec.get('repo')), \
+ 'Moving morphologies is not supported.'
+
+ def change_ref(self, repo_url, orig_ref, morph_name, new_ref):
+ '''Change a triplet's ref to a new one in all morphologies in a ref.
+
+ Change orig_ref to new_ref in any morphology that references the
+ original triplet. This includes stratum build-dependencies.
+
+ '''
+
+ def wanted_spec(m, kind, spec):
+ spec_name = spec['name'] if 'name' in spec else spec['morph']
+ return (spec.get('repo') == repo_url and
+ spec.get('ref') == orig_ref and
+ spec_name == morph_name)
+
+ def process_spec(m, kind, spec):
+ spec['unpetrify-ref'] = spec.get('ref')
+ spec['ref'] = new_ref
+ return True
+
+ self.traverse_specs(process_spec, wanted_spec)
+
+ def list_refs(self):
+ '''Return a set of all the (repo, ref) pairs in the MorphologySet.
+
+ This does not dirty the morphologies so they do not need to be
+ written back to the disk.
+
+ '''
+ known = set()
+
+ def wanted_spec(m, kind, spec):
+ return (spec.get('repo'), spec.get('ref')) not in known
+
+ def process_spec(m, kind, spec):
+ known.add((spec.get('repo'), spec.get('ref')))
+ return False
+
+ self.traverse_specs(process_spec, wanted_spec)
+
+ return known
+
+ def repoint_refs(self, repo_url, new_ref):
+ '''Change all specs which refer to (repo, *) to (repo, new_ref).
+
+ This is stunningly similar to change_ref, with the exception of
+ ignoring the morphology name and ref fields.
+
+ It is intended to be used before chunks are petrified
+
+ '''
+ def wanted_spec(m, kind, spec):
+ return spec.get('repo') == repo_url
+
+ def process_spec(m, kind, spec):
+ if 'unpetrify-ref' not in spec:
+ spec['unpetrify-ref'] = spec.get('ref')
+ spec['ref'] = new_ref
+ return True
+
+ self.traverse_specs(process_spec, wanted_spec)
+
+ def petrify_chunks(self, resolutions):
+ '''Update _every_ chunk's ref to the value resolved in resolutions.
+
+ `resolutions` must be a {(repo, ref): resolved_ref}
+
+ This is subtly different to change_ref, since that works on
+ changing a single spec including its filename, and the morphology
+ those specs refer to, while petrify_chunks is interested in changing
+ _all_ the refs.
+
+ '''
+
+ def wanted_chunk_spec(m, kind, spec):
+ # Do not attempt to petrify non-chunk specs.
+ # This is not handled by previous implementations, and
+ # the details are tricky.
+ if not (m['kind'] == 'stratum' and kind == 'chunks'):
+ return
+ ref = spec.get('ref')
+ return (not morphlib.git.is_valid_sha1(ref)
+ and (spec.get('repo'), ref) in resolutions)
+
+ def process_chunk_spec(m, kind, spec):
+ tup = (spec.get('repo'), spec.get('ref'))
+ spec['unpetrify-ref'] = spec.get('ref')
+ spec['ref'] = resolutions[tup]
+ return True
+
+ self.traverse_specs(process_chunk_spec, wanted_chunk_spec)