diff options
author | Jannis Pohlmann <jannis.pohlmann@codethink.co.uk> | 2012-01-17 17:29:06 +0000 |
---|---|---|
committer | Jannis Pohlmann <jannis.pohlmann@codethink.co.uk> | 2012-01-19 12:07:31 +0000 |
commit | 85069b20d623b93154fb3858eaae0e978fd6a2e6 (patch) | |
tree | 8bed2d00c77609b5974316b11ece7c54ceba6a5a | |
parent | f0531394958d2ce3fb44ca4e5f1ae204a599fda7 (diff) | |
download | morph-85069b20d623b93154fb3858eaae0e978fd6a2e6.tar.gz |
Initial work on integrate the build order work into builder.
-rwxr-xr-x | morph | 50 | ||||
-rw-r--r-- | morphlib/__init__.py | 1 | ||||
-rw-r--r-- | morphlib/blobs.py | 59 | ||||
-rw-r--r-- | morphlib/builddependencygraph.py | 374 | ||||
-rw-r--r-- | morphlib/builddependencygraph_tests.py | 12 | ||||
-rw-r--r-- | morphlib/builder.py | 343 | ||||
-rw-r--r-- | morphlib/morphology.py | 4 | ||||
-rw-r--r-- | morphlib/morphologyloader_tests.py | 2 | ||||
-rw-r--r-- | tests/show-dependencies.stdout | 242 |
9 files changed, 581 insertions, 506 deletions
@@ -20,14 +20,12 @@ import cliapp -import json import logging import os -import shutil -import tempfile import urlparse import morphlib +from morphlib.builddependencygraph import BuildDependencyGraph class Morph(cliapp.Application): @@ -78,7 +76,8 @@ class Morph(cliapp.Application): ''' tempdir = morphlib.tempdir.Tempdir() - builder = morphlib.builder.Builder(tempdir, self) + loader = morphlib.morphologyloader.MorphologyLoader(self.settings) + builder = morphlib.builder.Builder(tempdir, self, loader) if not os.path.exists(self.settings['cachedir']): os.mkdir(self.settings['cachedir']) @@ -87,8 +86,25 @@ class Morph(cliapp.Application): while len(args) >= 3: repo, ref, filename = args[:3] args = args[3:] - self.msg('Building %s - %s - %s' % (repo, ref, filename)) - ret.append(builder.build(repo, ref, filename)) + + # resolve the URL to the repository + base_url = self.settings['git-base-url'] + if not base_url.endswith('/'): + base_url += '/' + repo = urlparse.urljoin(base_url, repo) + if not repo.endswith('/'): + repo += '/' + + # derive a build order from the dependency graph + morphology = loader.load(repo, ref, filename) + graph = BuildDependencyGraph(loader, morphology) + graph.resolve() + blobs, order = graph.build_order() + + self.msg('Building %s' % morphology) + + # build things in this order + ret.append(builder.build(blobs, order)) # we may not have permission to tempdir.remove() ex = morphlib.execute.Execute('.', lambda msg: None) @@ -163,32 +179,32 @@ class Morph(cliapp.Application): if not base_url.endswith('/'): base_url += '/' repo = urlparse.urljoin(base_url, repo) + if not repo.endswith('/'): + repo += '/' # load the morphology corresponding to the build tuple loader = morphlib.morphologyloader.MorphologyLoader(self.settings) morphology = loader.load(repo, ref, filename) # create a dependency graph for the morphology - graph = \ - morphlib.builddependencygraph.BuildDependencyGraph(loader, - morphology) + graph = BuildDependencyGraph(loader, morphology) graph.resolve() # print the graph self.output.write('dependency tree:\n') - for node in graph.nodes: - self.output.write(' %s\n' % node) - for dep in node.dependencies: - self.output.write(' -> %s\n' % dep) + for blob in sorted(graph.blobs, key=str): + self.output.write(' %s\n' % blob) + for dependency in sorted(blob.dependencies, key=str): + self.output.write(' -> %s\n' % dependency) # compute a build order from the graph - order = graph.build_order() + blobs, order = graph.build_order() + sort_func = (lambda x,y : cmp(str(x), str(y))) self.output.write('build order:\n') for group in order: self.output.write(' group:\n') - for morphology in group: - self.output.write(' %s (%s)\n' % (morphology.name, - morphology.kind)) + for blob in sorted(group, key=str): + self.output.write(' %s\n' % blob) def msg(self, msg): '''Show a message to the user about what is going on.''' diff --git a/morphlib/__init__.py b/morphlib/__init__.py index 5302139d..d4346826 100644 --- a/morphlib/__init__.py +++ b/morphlib/__init__.py @@ -21,6 +21,7 @@ __version__ = '0.0' import bins +import blobs import builddependencygraph import builder import cachedir diff --git a/morphlib/blobs.py b/morphlib/blobs.py new file mode 100644 index 00000000..70cd1f90 --- /dev/null +++ b/morphlib/blobs.py @@ -0,0 +1,59 @@ +# Copyright (C) 2012 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. + + +class Blob(object): + + def __init__(self, parent, morph): + self.parent = parent + self.morph = morph + self.dependencies = set() + self.dependents = set() + + def add_dependency(self, other): + self.dependencies.add(other) + other.dependents.add(self) + + def remove_dependency(self, other): + self.dependencies.remove(other) + other.dependents.remove(self) + + def depends_on(self, other): + return other in self.dependencies + + @property + def chunks(self): + if self.morph.chunks: + return self.morph.chunks + else: + return { self.morph.name: ['.'] } + + def __str__(self): + return str(self.morph) + + +class Chunk(Blob): + + pass + + +class Stratum(Blob): + + pass + + +class System(Blob): + + pass diff --git a/morphlib/builddependencygraph.py b/morphlib/builddependencygraph.py index 7a432005..8fc85c20 100644 --- a/morphlib/builddependencygraph.py +++ b/morphlib/builddependencygraph.py @@ -15,195 +15,162 @@ import collections -import copy import os import morphlib -class Node(object): - - '''Node in the dependency graph.''' - - def __init__(self, morphology): - self.morphology = morphology - self.dependents = [] - self.dependencies = [] - - def add_dependency(self, other): - if other not in self.dependencies: - self.dependencies.append(other) - if self not in other.dependents: - other.dependents.append(self) - - def remove_dependency(self, other): - if other in self.dependencies: - self.dependencies.remove(other) - if self in other.dependents: - other.dependents.remove(self) - - def depends_on(self, other): - return other in self.dependencies - - def __str__(self): # pragma: no cover - return '%s (%s)' % (self.morphology.name, self.morphology.kind) - - def __deepcopy__(self, memo): # pragma: no cover - return Node(self.morphology) - - -class NodeList(list): - - def __deepcopy__(self, memo): # pragma: no cover - nodes = NodeList() - - old_to_new = {} - - for node in self: - node_copy = copy.deepcopy(node) - old_to_new[node] = node_copy - nodes.append(node_copy) - - for node in self: - node_copy = old_to_new[node] - for dep in node.dependencies: - dep_copy = old_to_new[dep] - node_copy.add_dependency(dep_copy) - - return nodes - - class BuildDependencyGraph(object): # pragma: no cover + + '''This class constructs a build dependency graph from an input morphology + and provides ways to traverse this graph. It also provides a method to + transform the dependency graph into groups of items that are independent + and can be built in parallel.''' - def __init__(self, loader, morphology): + def __init__(self, loader, morph): self.loader = loader - self.morphology = morphology - self.nodes = None + self.morph = morph + self.blobs = set() + + def create_blob(self, morph, parent=None): + '''Creates a blob from a morphology. The optional parent is used to + associate chunks with their containing stratum.''' - def add(self, morphology): - node = Node(morphology) - if node not in self.nodes: - self.nodes.append(node) + if morph.kind == 'stratum': + return morphlib.blobs.Stratum(parent, morph) + elif morph.kind == 'chunk': + return morphlib.blobs.Chunk(parent, morph) + else: + return morphlib.blobs.System(parent, morph) def resolve(self): - self.cached_morphologies = {} + '''Constructs the dependency graph by resolving dependencies + recursively.''' + + self.cached_blobs = {} self._resolve_strata() self._resolve_chunks() def build_order(self): - sorting = self._compute_topological_sorting() - - #print - #print 'sorting: %s' % [str(x) for x in sorting] - #print + '''Computes a topological sorting of the dependency graph and + generates a deque of groups, each of which contains a set + of items that are independent and can be built in parallel.''' - groups = [] - group_nodes = {} + sorting = self._compute_topological_sorting() + groups = collections.deque() - group = [] + # create the first group + group = set() groups.append(group) - for node in sorting: + # traverse the graph in topological order + for blob in sorting: + # add the current item to the current group, or a new group + # if one of its dependencies is in the current one create_group = False - for dep in node.dependencies: - if dep in group: + for dependency in blob.dependencies: + if dependency in group: create_group = True - if create_group: - group = [] + group = set() groups.append(group) + group.add(blob) - group.append(node) + # return the set of blobs and the build groups + return set(self.blobs), groups - morphology_groups = [] - for group in groups: - morphology_group = [] - for node in group: - morphology_group.append(node.morphology) - morphology_groups.append(morphology_group) + def _get_blob(self, info, parent=None): + '''Takes a (repo, ref, filename) tuple and looks up the blob for it. + It loads the corresponding morphology and blob on-demand if it is + not cached yet.''' - return morphology_groups + blob = self.cached_blobs.get(info, None) + if not blob: + morphology = self.loader.load(info[0], info[1], info[2]) + blob = self.create_blob(morphology, parent) + self.cached_blobs[info] = blob + return blob def _resolve_strata(self): - self.nodes = NodeList() + '''This method recursively generates a dependency graph of strata + for the input morphology using breadth-first search. It loads + morphologies and blobs on demand.''' - if self.morphology.kind == 'stratum': - root = Node(self.morphology) - self.nodes.append(root) + if self.morph.kind == 'stratum': + # turn the morphology into a stratum object + stratum = self.create_blob(self.morph) + # start the BFS at the input stratum queue = collections.deque() - queue.append(root) + queue.append(stratum) + self.blobs.add(stratum) + while len(queue) > 0: - node = queue.popleft() + stratum = queue.popleft() - if not node.morphology.build_depends: + # the DFS recursion ends whenever we have a stratum + # that depends on nothing else + if not stratum.morph.build_depends: continue - if isinstance(node.morphology.build_depends, list): - for other_name in node.morphology.build_depends: - # prepare a tuple for loading the morphology - repo = node.morphology.repo - ref = node.morphology.ref - filename = '%s.morph' % other_name + # verify that the build-depends format is valid + if isinstance(stratum.morph.build_depends, list): + for depname in stratum.morph.build_depends: + # prepare a tuple for the dependency stratum + repo = stratum.morph.repo + ref = stratum.morph.ref + filename = '%s.morph' % depname info = (repo, ref, filename) - # look up or create a node for the morphology - other_node = self.cached_morphologies.get(info, None) - if not other_node: - #print other_name - morphology = self.loader.load(repo, ref, filename) - other_node = Node(morphology) - - # cache the node for this morphology - self.cached_morphologies[info] = other_node + # load the dependency stratum on demand + depstratum = self._get_blob(info) - # add the morphology node to the graph - node.add_dependency(other_node) - self.nodes.append(other_node) - queue.append(other_node) + # add the dependency stratum to the graph + stratum.add_dependency(depstratum) + queue.append(depstratum) + self.blobs.add(depstratum) else: - raise Exception('%s uses an invalid "build-depends" format' % - os.path.basename(stratum_node.morphology.filename)) - - #print 'strata: %s' % [str(x) for x in self.nodes] + raise Exception('%s uses an invalid "build-depends" format' + % stratum) def _resolve_chunks(self): - strata_nodes = list(self.nodes) - for stratum_node in strata_nodes: - self._resolve_stratum_chunks(stratum_node) - - def _morphology_node(self, repo, ref, filename): - info = (repo, ref, filename) - - if info in self.cached_morphologies: - return self.cached_morphologies[info] - else: - morphology = self.loader.load(repo, ref, filename) - node = Node(morphology) - self.nodes.append(node) - self.cached_morphologies[info] = node - return node + '''Starting with a dependency graph of strata, this method fills the + graph with all contained chunks and creates dependencies where + appropriate. Chunk morphologies and blobs are loaded on demand.''' + + if self.morph.kind == 'chunk': + blob = self.create_blob(self.morph) + self.blobs.add(blob) + + blobs = list(self.blobs) + for blob in blobs: + if isinstance(blob, morphlib.blobs.Stratum): + self._resolve_stratum_chunks(blob) - def _resolve_stratum_chunks(self, stratum_node): - stratum_chunk_nodes = [] - chunk_lookup = {} + def _resolve_stratum_chunks(self, stratum): + # the set of chunks contained in the stratum + stratum_chunks = set() - # second, create nodes for all chunks in the stratum - for i in xrange(0, len(stratum_node.morphology.sources)): - source = stratum_node.morphology.sources[i] + # dictionary that maps chunk names to chunks + name_to_chunk = {} - # construct the build tuple + # create objects for all chunks in the stratum + for i in xrange(0, len(stratum.morph.sources)): + source = stratum.morph.sources[i] + + # construct a tuple for loading the chunk repo = source['repo'] ref = source['ref'] filename = '%s.morph' % (source['morph'] if 'morph' in source else source['name']) + info = (repo, ref, filename) - chunk_node = self._morphology_node(repo, ref, filename) - - chunk_lookup[source['name']] = chunk_node + # load the chunk on demand + chunk = self._get_blob(info, stratum) - stratum_chunk_nodes.append(chunk_node) + # store (name -> chunk) association to avoid loading the chunk twice + name_to_chunk[source['name']] = chunk # read the build-depends information build_depends = (source['build-depends'] @@ -212,80 +179,91 @@ class BuildDependencyGraph(object): # pragma: no cover # turn build-depends into proper dependencies in the graph if build_depends is None: - for j in xrange(0, i): - chunk_node.add_dependency(stratum_chunk_nodes[j]) + # chunks with no build-depends implicitly depend on all + # chunks listed earlier in the same stratum + for dependency in stratum_chunks: + chunk.add_dependency(dependency) elif isinstance(build_depends, list): for depname in build_depends: - if depname in chunk_lookup: - depnode = chunk_lookup[depname] - chunk_node.add_dependency(depnode) + if depname in name_to_chunk: + dependency = name_to_chunk[depname] + chunk.add_dependency(dependency) else: - raise Exception('%s: source "%s" references "%s" ' - 'before it is defined' % - (os.path.basename(stratum_node.morphology.filename), - source['name'], - depname)) + filename = os.path.basename(stratum.morph.filename) + raise Exception('%s: source %s references %s before it ' + 'is defined' % (filename, + source['name'], + depname)) else: - raise Exception('%s: source "%s" uses an invalid ' - '"build-depends" format' % - (os.path.basename(stratum_node.morphology.filename), - source['name'])) - - # make the chunk nodes in this stratum depend on all strata - # that need to be built first - for chunk_node in stratum_chunk_nodes: - for stratum_dep in stratum_node.dependencies: - chunk_node.add_dependency(stratum_dep) + filename = os.path.basename(stratum.morph.filename) + raise Exception('%s: source %s uses an invalid build-depends ' + 'format' % (filename, source['name'])) + + # add the chunk to stratum and graph + stratum_chunks.add(chunk) + self.blobs.add(chunk) + + # make the chunks in this stratum depend on all + # strata that need to be built first + for chunk in stratum_chunks: + for dependency in stratum.dependencies: + chunk.add_dependency(dependency) # clear the dependencies of the stratum - stratum_node.dependencies = [] + stratum.dependencies = set() - # make the stratum node depend on all its chunk nodes - for chunk_node in stratum_chunk_nodes: - stratum_node.add_dependency(chunk_node) + # make the stratum depend on all its chunks + for chunk in stratum_chunks: + stratum.add_dependency(chunk) def _compute_topological_sorting(self): - nodes = copy.deepcopy(self.nodes) + '''Computes a topological sorting of the dependency graph. A topological + sorting basically is the result of a series of breadth-first searches + starting at each leaf node (blobs with no dependencies). Blobs are + added to the sorting as soon as all their dependencies have been + added (which means that by then, all dependencies are satisfied). - original_node = {} - for node in self.nodes: - for node_copy in nodes: - if node.morphology == node_copy.morphology: - original_node[node_copy] = node + http://en.wikipedia.org/wiki/Topological_sorting.''' - #print 'compute topological sorting:' - #print ' nodes: %s' % [str(x) for x in nodes] + # map blobs to sets of satisfied dependencies. this is to detect when + # we can actually add blobs to the BFS queue. rather than dropping + # links between nodes, like most topological sorting algorithms do, + # we simply remember all satisfied dependencies and check if all + # of them are met repeatedly + satisfied_dependencies = {} - sorting = [] - leafs = collections.deque() - - for node in nodes: - if len(node.dependencies) is 0: - leafs.append(node) + # create an empty sorting + sorting = collections.deque() - #print ' leafs: %s' % [str(x) for x in leafs] + # create a set of leafs to start the DFS from + leafs = collections.deque() + for blob in self.blobs: + satisfied_dependencies[blob] = set() + if len(blob.dependencies) == 0: + leafs.append(blob) while len(leafs) > 0: - leaf = leafs.popleft() - sorting.append(leaf) - - #print ' visit %s' % leaf - - #print ' visit %s' % leaf - - for parent in list(leaf.dependents): - #print ' parent %s' % parent - #print ' parent %s dependencies: %s' % (parent, [str(x) for x in parent.dependencies]) - parent.remove_dependency(leaf) - #print ' parent %s dependencies: %s' % (parent, [str(x) for x in parent.dependencies]) - if len(parent.dependencies) == 0: - #print ' add %s' % parent - leafs.append(parent) - - #print [str(node) for node in sorting] - if len(sorting) < len(nodes): + # fetch a leaf blob from the DFS queue + blob = leafs.popleft() + + # add it to the sorting + sorting.append(blob) + + # mark this dependency as resolved + for dependent in blob.dependents: + satisfied_dependencies[dependent].add(blob) + + # add the dependent blob as a leaf if all + # its dependencies have been resolved + has = len(satisfied_dependencies[dependent]) + needs = len(dependent.dependencies) + if has == needs: + leafs.append(dependent) + + # if not all dependencies were resolved on the way, we + # have found at least one cyclic dependency + if len(sorting) < len(self.blobs): raise Exception('Cyclic dependencies found in the dependency ' - 'graph of "%s"' % self.morphology) + 'graph of "%s"' % self.morph) - #return sorting - return [original_node[node] for node in sorting] + return sorting diff --git a/morphlib/builddependencygraph_tests.py b/morphlib/builddependencygraph_tests.py index dec4e49c..ec3c5647 100644 --- a/morphlib/builddependencygraph_tests.py +++ b/morphlib/builddependencygraph_tests.py @@ -14,8 +14,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os -import StringIO import unittest import morphlib @@ -23,15 +21,9 @@ import morphlib class BuildDependencyGraphTests(unittest.TestCase): - def test_create_node_with_morphology(self): - fake_morphology = "fake morphology" - - node = morphlib.builddependencygraph.Node(fake_morphology) - self.assertEqual(node.morphology, fake_morphology) - def test_node_add_remove_dependency(self): - node1 = morphlib.builddependencygraph.Node(None) - node2 = morphlib.builddependencygraph.Node(None) + node1 = morphlib.blobs.Blob(None, None) + node2 = morphlib.blobs.Blob(None, None) node1.add_dependency(node2) diff --git a/morphlib/builder.py b/morphlib/builder.py index 95374e65..222e884c 100644 --- a/morphlib/builder.py +++ b/morphlib/builder.py @@ -17,10 +17,6 @@ import json import logging import os -import shutil -import StringIO -import tarfile -import urlparse import morphlib @@ -54,12 +50,11 @@ def ldconfig(ex, rootdir): else: logging.debug('No %s, not running ldconfig' % conf) -class BinaryBlob(object): - def __init__(self, morph, repo, ref): - self.morph = morph - self.repo = repo - self.ref = ref +class BlobBuilder(object): + + def __init__(self, blob): + self.blob = blob # The following MUST get set by the caller. self.builddir = None @@ -69,23 +64,66 @@ class BinaryBlob(object): self.msg = None self.cache_prefix = None self.tempdir = None - self.built = None + self.stage_items = [] self.dump_memory_profile = lambda msg: None # Stopwatch to measure build times self.build_watch = morphlib.stopwatch.Stopwatch() - def needs_built(self): - return [] - def builds(self): - raise NotImplemented() - + ret = {} + for chunk_name in self.blob.chunks: + ret[chunk_name] = self.filename(chunk_name) + return ret + def build(self): - raise NotImplemented() + # create the staging area on demand + if not os.path.exists(self.staging): + os.mkdir(self.staging) + + # record all items built in the process + built_items = [] + + # get a list of all the items we have to build for this blob + builds = self.builds() + + # if not all build items are in the cache, rebuild the blob + if not all(os.path.isfile(builds[name]) for name in builds): + with self.build_watch('overall-build'): + built_items += self.do_build() + + # check again, fail if not all build items were actually built + if not all(os.path.isfile(builds[name]) for name in builds): + raise Exception('Not all builds results expected from %s were ' + 'actually built' % self.blob) + + # install all build items to the staging area + for name, filename in builds.items(): + self.msg('Using cached %s %s at %s' % (self.blob.morph.kind, + name, filename)) + self.install_chunk(name, filename) + self.dump_memory_profile('after installing chunk') + + return built_items def filename(self, name): - return '%s.%s.%s' % (self.cache_prefix, self.morph.kind, name) + return '%s.%s.%s' % (self.cache_prefix, + self.blob.morph.kind, + name) + + def install_chunk(self, chunk_name, chunk_filename): + if self.blob.morph.kind != 'chunk': + return + if self.settings['bootstrap']: + self.msg('Unpacking item %s onto system' % chunk_name) + ex = morphlib.execute.Execute('/', self.msg) + morphlib.bins.unpack_binary(chunk_filename, '/', ex) + ldconfig(ex, '/') + else: + self.msg('Unpacking chunk %s into staging' % chunk_name) + ex = morphlib.execute.Execute(self.staging, self.msg) + morphlib.bins.unpack_binary(chunk_filename, self.staging, ex) + ldconfig(ex, self.staging) def prepare_binary_metadata(self, blob_name, **kwargs): '''Add metadata to a binary about to be built.''' @@ -93,8 +131,8 @@ class BinaryBlob(object): self.msg('Adding metadata to %s' % blob_name) meta = { 'name': blob_name, - 'kind': self.morph.kind, - 'description': self.morph.description, + 'kind': self.blob.morph.kind, + 'description': self.blob.morph.description, } for key, value in kwargs.iteritems(): meta[key] = value @@ -128,7 +166,7 @@ class BinaryBlob(object): self.write_cache_metadata(meta) -class Chunk(BinaryBlob): +class ChunkBuilder(BlobBuilder): build_system = { 'autotools': { @@ -147,21 +185,8 @@ class Chunk(BinaryBlob): }, } - @property - def chunks(self): - if self.morph.chunks: - return self.morph.chunks - else: - return { self.morph.name: ['.'] } - - def builds(self): - ret = {} - for chunk_name in self.chunks: - ret[chunk_name] = self.filename(chunk_name) - return ret - - def build(self): - logging.debug('Creating build tree at %s' % self.builddir) + def do_build(self): + self.msg('Creating build tree at %s' % self.builddir) self.ex = morphlib.execute.Execute(self.builddir, self.msg) self.setup_env() @@ -169,23 +194,40 @@ class Chunk(BinaryBlob): self.create_source_and_tarball() os.mkdir(self.destdir) - if self.morph.build_system: + if self.blob.morph.build_system: self.build_using_buildsystem() else: self.build_using_commands() self.dump_memory_profile('after building chunk') - chunks = self.create_chunks(self.chunks) - self.dump_memory_profile('after creating chunk blobs') + chunks = self.create_chunks() + self.dump_memory_profile('after creating build chunks') return chunks def setup_env(self): path = self.ex.env['PATH'] - tmpdir = self.ex.env.get('TMPDIR') tools = self.ex.env.get('BOOTSTRAP_TOOLS') distcc_hosts = self.ex.env.get('DISTCC_HOSTS') + + # copy a set of white-listed variables from the original env + copied_vars = dict.fromkeys([ + 'TMPDIR', + 'LD_PRELOAD', + 'LD_LIBRARY_PATH', + 'FAKEROOTKEY', + 'FAKED_MODE', + 'FAKEROOT_FD_BASE', + ], None) + for name in copied_vars: + copied_vars[name] = self.ex.env.get(name, None) + self.ex.env.clear() + # apply the copied variables to the clean env + for name in copied_vars: + if copied_vars[name] is not None: + self.ex.env[name] = copied_vars[name] + self.ex.env['TERM'] = 'dumb' self.ex.env['SHELL'] = '/bin/sh' self.ex.env['USER'] = \ @@ -193,8 +235,6 @@ class Chunk(BinaryBlob): self.ex.env['LOGNAME'] = 'tomjon' self.ex.env['LC_ALL'] = 'C' self.ex.env['HOME'] = os.path.join(self.tempdir.dirname) - if tmpdir is not None: - self.ex.env['TMPDIR'] = tmpdir if self.settings['keep-path'] or self.settings['bootstrap']: self.ex.env['PATH'] = path @@ -215,8 +255,8 @@ class Chunk(BinaryBlob): if distcc_hosts is not None: self.ex.env['DISTCC_HOSTS'] = distcc_hosts - if self.morph.max_jobs: - max_jobs = int(self.morph.max_jobs) + if self.blob.morph.max_jobs: + max_jobs = int(self.blob.morph.max_jobs) logging.debug('max_jobs from morph: %s' % max_jobs) elif self.settings['max-jobs']: max_jobs = self.settings['max-jobs'] @@ -244,8 +284,8 @@ class Chunk(BinaryBlob): 'for chunk') tarball = self.cache_prefix + '.src.tar' #FIXME Ugh use treeish everwhere - path = urlparse.urlparse(self.repo).path - t = morphlib.git.Treeish (path, self.ref) + path = urlparse.urlparse(self.blob.morph.repo).path + t = morphlib.git.Treeish(path, self.blob.morph.ref) morphlib.git.export_sources(t, tarball) self.dump_memory_profile('after exporting sources') os.mkdir(self.builddir) @@ -254,7 +294,7 @@ class Chunk(BinaryBlob): 'for chunk') def build_using_buildsystem(self): - bs_name = self.morph.build_system + bs_name = self.blob.morph.build_system self.msg('Building using well-known build system %s' % bs_name) bs = self.build_system[bs_name] self.run_sequentially('configure', bs['configure-commands']) @@ -264,10 +304,10 @@ class Chunk(BinaryBlob): def build_using_commands(self): self.msg('Building using explicit commands') - self.run_sequentially('configure', self.morph.configure_commands) - self.run_in_parallel('build', self.morph.build_commands) - self.run_sequentially('test', self.morph.test_commands) - self.run_sequentially('install', self.morph.install_commands) + self.run_sequentially('configure', self.blob.morph.configure_commands) + self.run_in_parallel('build', self.blob.morph.build_commands) + self.run_sequentially('test', self.blob.morph.test_commands) + self.run_sequentially('install', self.blob.morph.install_commands) def run_in_parallel(self, what, commands): self.msg('commands: %s' % what) @@ -284,58 +324,49 @@ class Chunk(BinaryBlob): self.ex.env['MAKEFLAGS'] = flags logging.debug('Restore MAKEFLAGS=%s' % self.ex.env['MAKEFLAGS']) - def create_chunks(self, chunks): - ret = {} + def create_chunks(self): + chunks = [] with self.build_watch('create-chunks'): - for chunk_name in chunks: + for chunk_name in self.blob.chunks: self.msg('Creating chunk %s' % chunk_name) self.prepare_binary_metadata(chunk_name) - patterns = chunks[chunk_name] + patterns = self.blob.chunks[chunk_name] patterns += [r'baserock/%s\.' % chunk_name] filename = self.filename(chunk_name) self.msg('Creating binary for %s' % chunk_name) morphlib.bins.create_chunk(self.destdir, filename, patterns, self.ex, self.dump_memory_profile) - ret[chunk_name] = filename + chunks.append((chunk_name, filename)) files = os.listdir(self.destdir) if files: raise Exception('DESTDIR %s is not empty: %s' % (self.destdir, files)) - return ret + return chunks -class Stratum(BinaryBlob): +class StratumBuilder(BlobBuilder): - def needs_built(self): - for source in self.morph.sources: - project_name = source['name'] - morph_name = source['morph'] if 'morph' in source else project_name - repo = source['repo'] - ref = source['ref'] - chunks = source['chunks'] if 'chunks' in source else [project_name] - yield repo, ref, morph_name, chunks - def builds(self): - filename = self.filename(self.morph.name) - return { self.morph.name: filename } + filename = self.filename(self.blob.morph.name) + return { self.blob.morph.name: filename } - def build(self): + def do_build(self): os.mkdir(self.destdir) ex = morphlib.execute.Execute(self.destdir, self.msg) with self.build_watch('unpack-chunks'): - for chunk_name, filename in self.built: + for chunk_name, filename in self.stage_items: self.msg('Unpacking chunk %s' % chunk_name) morphlib.bins.unpack_binary(filename, self.destdir, ex) with self.build_watch('create-binary'): - self.prepare_binary_metadata(self.morph.name) - self.msg('Creating binary for %s' % self.morph.name) - filename = self.filename(self.morph.name) + self.prepare_binary_metadata(self.blob.morph.name) + self.msg('Creating binary for %s' % self.blob.morph.name) + filename = self.filename(self.blob.morph.name) morphlib.bins.create_stratum(self.destdir, filename, ex) - return { self.morph.name: filename } + return { self.blob.morph.name: filename } -class System(BinaryBlob): +class SystemBuilder(BlobBuilder): def needs_built(self): for stratum_name in self.morph.strata: @@ -345,7 +376,7 @@ class System(BinaryBlob): filename = self.filename(self.morph.name) return { self.morph.name: filename } - def build(self): + def do_build(self): self.ex = morphlib.execute.Execute(self.tempdir.dirname, self.msg) # Create image. @@ -391,7 +422,7 @@ class System(BinaryBlob): for name, filename in self.built: self.msg('unpack %s from %s' % (name, filename)) self.ex.runv(['tar', '-C', mount_point, '-xf', filename]) - ldconfig(ex, mount_point) + ldconfig(self.ex, mount_point) # Create fstab. with self.build_watch('create-fstab'): @@ -426,7 +457,7 @@ append root=/dev/sda1 init=/sbin/init quiet rw # Unmount. with self.build_watch('unmount-filesystem'): self.ex.runv(['umount', mount_point]) - except BaseException, e: + except BaseException: # Unmount. if mount_point is not None: try: @@ -458,15 +489,14 @@ class Builder(object): The objects may be chunks or strata.''' - def __init__(self, tempdir, app): + def __init__(self, tempdir, app, morph_loader): self.tempdir = tempdir self.real_msg = app.msg self.settings = app.settings self.dump_memory_profile = app.dump_memory_profile self.cachedir = morphlib.cachedir.CacheDir(self.settings['cachedir']) self.indent = 0 - self.morph_loader = \ - morphlib.morphologyloader.MorphologyLoader(self.settings) + self.morph_loader = morph_loader def msg(self, text): spaces = ' ' * self.indent @@ -478,94 +508,91 @@ class Builder(object): def indent_less(self): self.indent -= 1 - def build(self, repo, ref, filename): - '''Build a binary based on a morphology.''' + def build(self, blobs, build_order): + '''Build a list of groups of morphologies. Items in a group + can be built in parallel.''' - self.dump_memory_profile('at start of build method') self.indent_more() - self.msg('build %s|%s|%s' % (repo, ref, filename)) - morph = self.morph_loader.load(repo, ref, filename) - repo = morph.repo - self.dump_memory_profile('after getting morph from git') - if morph.kind == 'chunk': - blob = Chunk(morph, repo, ref) - elif morph.kind == 'stratum': - blob = Stratum(morph, repo, ref) - elif morph.kind == 'system': - blob = System(morph, repo, ref) - else: - raise Exception('Unknown kind of morphology: %s' % morph.kind) - self.dump_memory_profile('after creating Chunk/Stratum/...') + # first pass: create builders for all blobs + builders = {} + for group in build_order: + for blob in group: + builder = self.create_blob_builder(blob) + builders[blob] = builder + + # second pass: build group by group, item after item + ret = [] + while len(build_order) > 0: + group = build_order.popleft() + while len(group) > 0: + blob = group.pop() + + self.msg('Building %s' % blob) + self.indent_more() - cache_id = self.get_cache_id(repo, ref, filename) - logging.debug('cachae id: %s' % repr(cache_id)) - self.dump_memory_profile('after computing cache id') + ## TODO this needs to be done recursively + ## make sure all dependencies are in the staging area + #for dependency in blob.dependencies: + # depbuilder = builders[dependency] + # depbuilder.stage() - blob.builddir = self.tempdir.join('%s.build' % morph.name) - blob.destdir = self.tempdir.join('%s.inst' % morph.name) - blob.staging = self.tempdir.join('staging') - if not os.path.exists(blob.staging): - os.mkdir(blob.staging) - blob.settings = self.settings - blob.msg = self.msg - blob.cache_prefix = self.cachedir.name(cache_id) - blob.tempdir = self.tempdir - blob.dump_memory_profile = self.dump_memory_profile - - builds = blob.builds() - self.dump_memory_profile('after blob.builds()') - if all(os.path.exists(builds[x]) for x in builds): - for x in builds: - self.msg('using cached %s %s at %s' % - (morph.kind, x, builds[x])) - self.install_chunk(morph, x, builds[x], blob.staging) - self.dump_memory_profile('after installing chunk') - built = builds - else: - with blob.build_watch('overall-build'): + ## TODO this needs the set of recursively collected + ## dependencies + ## make sure all non-dependencies are not staged + #for nondependency in (blobs - blob.dependencies): + # depbuilder = builders[nondependency] + # depbuilder.unstage() - with blob.build_watch('build-needed'): - self.build_needed(blob) - self.dump_memory_profile('after building needed') + built_items = builders[blob].build() + + if blob.parent: + for item, filename in built_items: + self.msg('Marking %s to be staged for %s' % + (item, blob.parent)) - self.msg('Building %s %s' % (morph.kind, morph.name)) - self.indent_more() - built = blob.build() - self.dump_memory_profile('after building blob') - self.indent_less() - for x in built: - self.msg('%s %s cached at %s' % (morph.kind, x, built[x])) - self.install_chunk(morph, x, built[x], blob.staging) - self.dump_memory_profile('after installing chunks') + parent_builder = builders[blob.parent] + parent_builder.stage_items += built_items - blob.save_build_times() + self.indent_less() self.indent_less() - self.dump_memory_profile('at end of build method') - return morph, built - - def build_needed(self, blob): - blob.built = [] - for repo, ref, morph_name, blob_names in blob.needs_built(): - morph_filename = '%s.morph' % morph_name - morph, cached = self.build(repo, ref, morph_filename) - for blob_name in blob_names: - blob.built.append((blob_name, cached[blob_name])) - - def install_chunk(self, morph, chunk_name, chunk_filename, staging_dir): - if morph.kind != 'chunk': - return - if self.settings['bootstrap']: - self.msg('Unpacking chunk %s onto system' % chunk_name) - ex = morphlib.execute.Execute('/', self.msg) - morphlib.bins.unpack_binary(chunk_filename, '/', ex) - ldconfig(ex, '/') + + return ret + + def create_blob_builder(self, blob): + if isinstance(blob, morphlib.blobs.Stratum): + builder = StratumBuilder(blob) + elif isinstance(blob, morphlib.blobs.Chunk): + builder = ChunkBuilder(blob) + elif isinstance(blob, morphlib.blobs.System): + builder = SystemBuilder(blob) else: - self.msg('Unpacking chunk %s into staging' % chunk_name) - ex = morphlib.execute.Execute(staging_dir, self.msg) - morphlib.bins.unpack_binary(chunk_filename, staging_dir, ex) - ldconfig(ex, staging_dir) + raise TypeError('Blob %s has unknown type %s' % + (str(blob), type(blob))) + + cache_id = self.get_blob_cache_id(blob) + logging.debug('cache id: %s' % repr(cache_id)) + self.dump_memory_profile('after computing cache id') + + builder.builddir = self.tempdir.join('%s.build' % blob.morph.name) + builder.destdir = self.tempdir.join('%s.inst' % blob.morph.name) + builder.staging = self.tempdir.join('staging') + builder.settings = self.settings + builder.msg = self.msg + builder.cache_prefix = self.cachedir.name(cache_id) + builder.tempdir = self.tempdir + builder.dump_memory_profile = self.dump_memory_profile + + return builder + + def get_blob_cache_id(self, blob): + # FIXME os.path.basename() only works if the .morph file is an + # immediate children of the repo location and is not located in + # a subfolder + return self.get_cache_id(blob.morph.repo, + blob.morph.ref, + os.path.basename(blob.morph.filename)) def get_cache_id(self, repo, ref, morph_filename): logging.debug('get_cache_id(%s, %s, %s)' % diff --git a/morphlib/morphology.py b/morphlib/morphology.py index 8f787939..c530824c 100644 --- a/morphlib/morphology.py +++ b/morphlib/morphology.py @@ -16,6 +16,7 @@ import json import logging +import os class Morphology(object): @@ -123,3 +124,6 @@ class Morphology(object): else: return url + def __str__(self): # pragma: no cover + return '%s|%s|%s' % (os.path.basename(os.path.dirname(self.repo)), + self.ref, os.path.basename(self.filename)) diff --git a/morphlib/morphologyloader_tests.py b/morphlib/morphologyloader_tests.py index f9a03915..475ce391 100644 --- a/morphlib/morphologyloader_tests.py +++ b/morphlib/morphologyloader_tests.py @@ -14,8 +14,6 @@ # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. -import os -import StringIO import unittest import morphlib diff --git a/tests/show-dependencies.stdout b/tests/show-dependencies.stdout index 6f0096e2..b8357ad8 100644 --- a/tests/show-dependencies.stdout +++ b/tests/show-dependencies.stdout @@ -1,132 +1,132 @@ dependency tree: - xfce-core (stratum) - -> libxfce4util (chunk) - -> xfconf (chunk) - -> libxfce4ui (chunk) - -> exo (chunk) - -> garcon (chunk) - -> thunar (chunk) - -> tumbler (chunk) - -> xfce4-panel (chunk) - -> xfce4-settings (chunk) - -> xfce4-session (chunk) - -> xfwm4 (chunk) - -> xfdesktop (chunk) - -> xfce4-appfinder (chunk) - -> gtk-xfce-engine (chunk) - gtk-stack (stratum) - -> freetype (chunk) - -> fontconfig (chunk) - -> cairo (chunk) - -> pango (chunk) - -> glib (chunk) - -> gdk-pixbuf (chunk) - -> gtk (chunk) - -> dbus (chunk) - -> dbus-glib (chunk) - libxfce4util (chunk) - -> gtk-stack (stratum) - xfconf (chunk) - -> libxfce4util (chunk) - -> gtk-stack (stratum) - libxfce4ui (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - exo (chunk) - -> libxfce4util (chunk) - -> gtk-stack (stratum) - garcon (chunk) - -> libxfce4util (chunk) - -> gtk-stack (stratum) - thunar (chunk) - -> libxfce4ui (chunk) - -> exo (chunk) - -> gtk-stack (stratum) - tumbler (chunk) - -> gtk-stack (stratum) - xfce4-panel (chunk) - -> libxfce4ui (chunk) - -> exo (chunk) - -> garcon (chunk) - -> gtk-stack (stratum) - xfce4-settings (chunk) - -> libxfce4ui (chunk) - -> exo (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - xfce4-session (chunk) - -> libxfce4ui (chunk) - -> exo (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - xfwm4 (chunk) - -> libxfce4ui (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - xfdesktop (chunk) - -> libxfce4ui (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - xfce4-appfinder (chunk) - -> libxfce4ui (chunk) - -> garcon (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - gtk-xfce-engine (chunk) - -> libxfce4ui (chunk) - -> garcon (chunk) - -> xfconf (chunk) - -> gtk-stack (stratum) - freetype (chunk) - fontconfig (chunk) - cairo (chunk) - pango (chunk) - -> freetype (chunk) - -> fontconfig (chunk) - glib (chunk) - gdk-pixbuf (chunk) - -> glib (chunk) - gtk (chunk) - -> cairo (chunk) - -> gdk-pixbuf (chunk) - -> glib (chunk) - -> pango (chunk) - dbus (chunk) - dbus-glib (chunk) - -> dbus (chunk) - -> glib (chunk) + test-repo|master|cairo.morph + test-repo|master|dbus-glib.morph + -> test-repo|master|dbus.morph + -> test-repo|master|glib.morph + test-repo|master|dbus.morph + test-repo|master|exo.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4util.morph + test-repo|master|fontconfig.morph + test-repo|master|freetype.morph + test-repo|master|garcon.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4util.morph + test-repo|master|gdk-pixbuf.morph + -> test-repo|master|glib.morph + test-repo|master|glib.morph + test-repo|master|gtk-stack.morph + -> test-repo|master|cairo.morph + -> test-repo|master|dbus-glib.morph + -> test-repo|master|dbus.morph + -> test-repo|master|fontconfig.morph + -> test-repo|master|freetype.morph + -> test-repo|master|gdk-pixbuf.morph + -> test-repo|master|glib.morph + -> test-repo|master|gtk.morph + -> test-repo|master|pango.morph + test-repo|master|gtk-xfce-engine.morph + -> test-repo|master|garcon.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph + test-repo|master|gtk.morph + -> test-repo|master|cairo.morph + -> test-repo|master|gdk-pixbuf.morph + -> test-repo|master|glib.morph + -> test-repo|master|pango.morph + test-repo|master|libxfce4ui.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|xfconf.morph + test-repo|master|libxfce4util.morph + -> test-repo|master|gtk-stack.morph + test-repo|master|pango.morph + -> test-repo|master|fontconfig.morph + -> test-repo|master|freetype.morph + test-repo|master|thunar.morph + -> test-repo|master|exo.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + test-repo|master|tumbler.morph + -> test-repo|master|gtk-stack.morph + test-repo|master|xfce-core.morph + -> test-repo|master|exo.morph + -> test-repo|master|garcon.morph + -> test-repo|master|gtk-xfce-engine.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|libxfce4util.morph + -> test-repo|master|thunar.morph + -> test-repo|master|tumbler.morph + -> test-repo|master|xfce4-appfinder.morph + -> test-repo|master|xfce4-panel.morph + -> test-repo|master|xfce4-session.morph + -> test-repo|master|xfce4-settings.morph + -> test-repo|master|xfconf.morph + -> test-repo|master|xfdesktop.morph + -> test-repo|master|xfwm4.morph + test-repo|master|xfce4-appfinder.morph + -> test-repo|master|garcon.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph + test-repo|master|xfce4-panel.morph + -> test-repo|master|exo.morph + -> test-repo|master|garcon.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + test-repo|master|xfce4-session.morph + -> test-repo|master|exo.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph + test-repo|master|xfce4-settings.morph + -> test-repo|master|exo.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph + test-repo|master|xfconf.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4util.morph + test-repo|master|xfdesktop.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph + test-repo|master|xfwm4.morph + -> test-repo|master|gtk-stack.morph + -> test-repo|master|libxfce4ui.morph + -> test-repo|master|xfconf.morph build order: group: - freetype (chunk) - fontconfig (chunk) - cairo (chunk) - glib (chunk) - dbus (chunk) + test-repo|master|cairo.morph + test-repo|master|dbus.morph + test-repo|master|fontconfig.morph + test-repo|master|freetype.morph + test-repo|master|glib.morph group: - pango (chunk) - gdk-pixbuf (chunk) - dbus-glib (chunk) + test-repo|master|dbus-glib.morph + test-repo|master|gdk-pixbuf.morph + test-repo|master|pango.morph group: - gtk (chunk) + test-repo|master|gtk.morph group: - gtk-stack (stratum) + test-repo|master|gtk-stack.morph group: - libxfce4util (chunk) - tumbler (chunk) + test-repo|master|libxfce4util.morph + test-repo|master|tumbler.morph group: - xfconf (chunk) - exo (chunk) - garcon (chunk) + test-repo|master|exo.morph + test-repo|master|garcon.morph + test-repo|master|xfconf.morph group: - libxfce4ui (chunk) + test-repo|master|libxfce4ui.morph group: - thunar (chunk) - xfce4-panel (chunk) - xfce4-settings (chunk) - xfce4-session (chunk) - xfwm4 (chunk) - xfdesktop (chunk) - xfce4-appfinder (chunk) - gtk-xfce-engine (chunk) + test-repo|master|gtk-xfce-engine.morph + test-repo|master|thunar.morph + test-repo|master|xfce4-appfinder.morph + test-repo|master|xfce4-panel.morph + test-repo|master|xfce4-session.morph + test-repo|master|xfce4-settings.morph + test-repo|master|xfdesktop.morph + test-repo|master|xfwm4.morph group: - xfce-core (stratum) + test-repo|master|xfce-core.morph |