summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJannis Pohlmann <jannis.pohlmann@codethink.co.uk>2012-01-17 17:29:06 +0000
committerJannis Pohlmann <jannis.pohlmann@codethink.co.uk>2012-01-19 12:07:31 +0000
commit85069b20d623b93154fb3858eaae0e978fd6a2e6 (patch)
tree8bed2d00c77609b5974316b11ece7c54ceba6a5a
parentf0531394958d2ce3fb44ca4e5f1ae204a599fda7 (diff)
downloadmorph-85069b20d623b93154fb3858eaae0e978fd6a2e6.tar.gz
Initial work on integrate the build order work into builder.
-rwxr-xr-xmorph50
-rw-r--r--morphlib/__init__.py1
-rw-r--r--morphlib/blobs.py59
-rw-r--r--morphlib/builddependencygraph.py374
-rw-r--r--morphlib/builddependencygraph_tests.py12
-rw-r--r--morphlib/builder.py343
-rw-r--r--morphlib/morphology.py4
-rw-r--r--morphlib/morphologyloader_tests.py2
-rw-r--r--tests/show-dependencies.stdout242
9 files changed, 581 insertions, 506 deletions
diff --git a/morph b/morph
index 360b9a7f..413944db 100755
--- a/morph
+++ b/morph
@@ -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