summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rwxr-xr-xcheck4
-rwxr-xr-xmorph47
-rw-r--r--morphlib/__init__.py1
-rw-r--r--morphlib/builder.py147
-rw-r--r--morphlib/fsutils.py72
-rw-r--r--tests.as-root/hello-chunk.tar.gzbin0 -> 12596 bytes
-rw-r--r--tests.as-root/hello-stratum.morph10
-rwxr-xr-xtests.as-root/make-patch.script86
-rw-r--r--tests.as-root/make-patch.stderr16
-rw-r--r--tests.as-root/make-patch.stdout3
-rwxr-xr-xtests.as-root/morph29
-rwxr-xr-xtests.as-root/setup128
-rwxr-xr-xtests/morph7
-rw-r--r--without-test-modules1
14 files changed, 475 insertions, 76 deletions
diff --git a/check b/check
index eaec3a1d..8af05668 100755
--- a/check
+++ b/check
@@ -22,6 +22,10 @@ set -e
python setup.py clean check
cmdtest tests
cmdtest tests.branching
+if [ $(whoami) = root ] && command -v mkfs.btrfs > /dev/null
+then
+ cmdtest tests.as-root
+fi
if [ -d .git ];
then
diff --git a/morph b/morph
index e44d4427..754f3b8e 100755
--- a/morph
+++ b/morph
@@ -356,6 +356,53 @@ class Morph(cliapp.Application):
tempdir.remove()
+ def cmd_make_patch(self, args):
+ assert os.path.exists(self.settings['cachedir'])
+
+ tempdir = morphlib.tempdir.Tempdir(self.settings['tempdir'])
+ morph_loader = MorphologyLoader(self.settings)
+ src_manager = morphlib.sourcemanager.SourceManager(self, update=False)
+ factory = morphlib.builder.Factory(tempdir)
+ factory.create_staging()
+ builder = morphlib.builder.Builder(tempdir, self, morph_loader,
+ src_manager, factory)
+ #cachedir = morphlib.cachedir.CacheDir(self.settings['cachedir'])
+ ex = morphlib.execute.Execute('.', self.msg)
+
+ outpath = args[0]
+ args = args[1:]
+ trip_iter = self._itertriplets(args)
+ paths = {}
+ for name in ('source', 'target'):
+ repo, ref, filename = trip_iter.next()
+ print repo, ref, filename
+ treeish = src_manager.get_treeish(repo, ref)
+ morph = morph_loader.load(treeish, filename)
+ blob = morphlib.blobs.Blob.create_blob(morph)
+ blob_builder = builder.create_blob_builder(blob)
+ paths[name] = blob_builder.filename(morph.name)
+
+ try:
+ for name in paths.iterkeys():
+ # mount the system images
+ part = morphlib.fsutils.setup_device_mapping(ex, paths[name])
+ mount_point = tempdir.join('mnt_' + name)
+ morphlib.fsutils.mount(ex, part, mount_point)
+
+ # make a diff
+ ex.runv(['tbdiff-create', outpath,
+ os.path.join(tempdir.join('mnt_source'), 'factory'),
+ os.path.join(tempdir.join('mnt_target'), 'factory')])
+ except Exception:
+ raise
+ finally:
+ # cleanup
+ for name in paths.iterkeys():
+ mount_point = tempdir.join('mnt_' + name)
+ morphlib.fsutils.unmount(ex, mount_point)
+ morphlib.fsutils.undo_device_mapping(ex, paths[name])
+ factory.remove_staging()
+
def cmd_init(self, args):
'''Initialize a mine.'''
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
index 532c29a4..05f4022b 100644
--- a/morphlib/__init__.py
+++ b/morphlib/__init__.py
@@ -25,6 +25,7 @@ import buildworker
import builder
import cachedir
import execute
+import fsutils
import git
import morphology
import morphologyloader
diff --git a/morphlib/builder.py b/morphlib/builder.py
index 05298994..2463108e 100644
--- a/morphlib/builder.py
+++ b/morphlib/builder.py
@@ -429,8 +429,15 @@ class SystemBuilder(BlobBuilder): # pragma: no cover
self._create_fs(partition)
mount_point = self.tempdir.join('mnt')
self._mount(partition, mount_point)
- self._unpack_strata(mount_point)
- self._create_fstab(mount_point)
+ factory_path = os.path.join(mount_point, 'factory')
+ self._create_subvolume(factory_path)
+ self._unpack_strata(factory_path)
+ self._create_fstab(factory_path)
+ self._create_extlinux_config(factory_path)
+ self._create_subvolume_snapshot(
+ mount_point, 'factory', 'factory-run')
+ factory_run_path = os.path.join(mount_point, 'factory-run')
+ self._install_boot_files(factory_run_path, mount_point)
self._install_extlinux(mount_point)
self._unmount(mount_point)
except BaseException:
@@ -443,100 +450,92 @@ class SystemBuilder(BlobBuilder): # pragma: no cover
def _create_image(self, image_name):
with self.build_watch('create-image'):
- # FIXME: This could be done in pure python, no need to run dd
- self.ex.runv(['dd', 'if=/dev/zero', 'of=' + image_name, 'bs=1',
- 'seek=%d' % self.blob.morph.disk_size,
- 'count=0'])
+ morphlib.fsutils.create_image(self.ex, image_name,
+ self.blob.morph.disk_size)
def _partition_image(self, image_name):
with self.build_watch('partition-image'):
- self.ex.runv(['sfdisk', image_name], feed_stdin='1,,83,*\n')
+ morphlib.fsutils.partition_image(self.ex, image_name)
def _install_mbr(self, image_name):
with self.build_watch('install-mbr'):
- for path in ['/usr/lib/extlinux/mbr.bin',
- '/usr/share/syslinux/mbr.bin']:
- if os.path.exists(path):
- self.ex.runv(['dd', 'if=' + path, 'of=' + image_name,
- 'conv=notrunc'])
- break
+ morphlib.fsutils.install_mbr(self.ex, image_name)
def _setup_device_mapping(self, image_name):
with self.build_watch('setup-device-mapper'):
- out = self.ex.runv(['sfdisk', '-d', image_name])
- for line in out.splitlines():
- words = line.split()
- if (len(words) >= 4 and
- words[2] == 'start=' and
- words[3] != '0,'):
- n = int(words[3][:-1]) # skip trailing comma
- start = n * 512
- break
-
- self.ex.runv(['losetup', '-o', str(start), '-f', image_name])
-
- out = self.ex.runv(['losetup', '-j', image_name])
- line = out.strip()
- i = line.find(':')
- return line[:i]
+ return morphlib.fsutils.setup_device_mapping(self.ex, image_name)
def _create_fs(self, partition):
with self.build_watch('create-filesystem'):
- # FIXME: the hardcoded size is icky but the default broke
- self.ex.runv(['mkfs', '-t', 'ext4', '-q', partition, '4194304'])
+ morphlib.fsutils.create_fs(self.ex, partition)
def _mount(self, partition, mount_point):
with self.build_watch('mount-filesystem'):
- os.mkdir(mount_point)
- self.ex.runv(['mount', partition, mount_point])
+ morphlib.fsutils.mount(self.ex, partition, mount_point)
+
+ def _create_subvolume(self, path):
+ with self.build_watch('create-factory-subvolume'):
+ self.ex.runv(['btrfs', 'subvolume', 'create', path])
- def _unpack_strata(self, mount_point):
+ def _unpack_strata(self, path):
with self.build_watch('unpack-strata'):
for name, filename in self.stage_items:
self.msg('unpack %s from %s' % (name, filename))
- self.ex.runv(['tar', '-C', mount_point, '-xhf', filename])
- ldconfig(self.ex, mount_point)
+ self.ex.runv(['tar', '-C', path, '-xhf', filename])
+ ldconfig(self.ex, path)
- def _create_fstab(self, mount_point):
+ def _create_fstab(self, path):
with self.build_watch('create-fstab'):
- fstab = os.path.join(mount_point, 'etc', 'fstab')
+ fstab = os.path.join(path, 'etc', 'fstab')
if not os.path.exists(os.path.dirname(fstab)):
os.makedirs(os.path.dirname(fstab))
with open(fstab, 'w') as f:
f.write('proc /proc proc defaults 0 0\n')
f.write('sysfs /sys sysfs defaults 0 0\n')
- f.write('/dev/sda1 / ext4 errors=remount-ro 0 1\n')
-
- def _install_extlinux(self, mount_point):
+ f.write('/dev/sda1 / btrfs errors=remount-ro 0 1\n')
+
+ def _create_extlinux_config(self, path):
+ config = os.path.join(path, 'extlinux.conf')
+ with open(config, 'w') as f:
+ f.write('default linux\n')
+ f.write('timeout 1\n')
+ f.write('label linux\n')
+ f.write('kernel /boot/vmlinuz\n')
+ f.write('append root=/dev/sda1 rootflags=subvol=factory-run '
+ 'init=/sbin/init quiet rw\n')
+
+ def _create_subvolume_snapshot(self, path, source, target):
+ with self.build_watch('create-subvolume-snapshot'):
+ self.ex.runv(['btrfs', 'subvolume', 'snapshot', source, target],
+ cwd=path)
+
+ def _install_boot_files(self, sourcefs, targetfs):
+ with self.build_watch('install-boot-files'):
+ logging.debug('installing boot files into root volume')
+ shutil.copy2(os.path.join(sourcefs, 'extlinux.conf'),
+ os.path.join(targetfs, 'extlinux.conf'))
+ os.mkdir(os.path.join(targetfs, 'boot'))
+ shutil.copy2(os.path.join(sourcefs, 'boot', 'vmlinuz'),
+ os.path.join(targetfs, 'boot', 'vmlinuz'))
+ shutil.copy2(os.path.join(sourcefs, 'boot', 'System.map'),
+ os.path.join(targetfs, 'boot', 'System.map'))
+
+ def _install_extlinux(self, path):
with self.build_watch('install-bootloader'):
- conf = os.path.join(mount_point, 'extlinux.conf')
- logging.debug('configure extlinux %s' % conf)
- with open(conf, 'w') as f:
- f.write('default linux\n')
- f.write('timeout 1\n')
- f.write('label linux\n')
- f.write('kernel /boot/vmlinuz\n')
- f.write('append root=/dev/sda1 init=/sbin/init quiet rw\n')
-
- self.ex.runv(['extlinux', '--install', mount_point])
-
- # Weird hack that makes extlinux work.
- # FIXME: There is a bug somewhere.
+ self.ex.runv(['extlinux', '--install', path])
+
+ # FIXME this hack seems to be necessary to let extlinux finish
self.ex.runv(['sync'])
time.sleep(2)
def _unmount(self, mount_point):
if mount_point is not None:
with self.build_watch('unmount-filesystem'):
- self.ex.runv(['umount', mount_point])
+ morphlib.fsutils.unmount(self.ex, mount_point)
def _undo_device_mapping(self, image_name):
with self.build_watch('undo-device-mapper'):
- out = self.ex.runv(['losetup', '-j', image_name])
- for line in out.splitlines():
- i = line.find(':')
- device = line[:i]
- self.ex.runv(['losetup', '-d', device])
+ morphlib.fsutils.undo_device_mapping(self.ex, image_name)
def _move_image_to_cache(self, image_name):
with self.build_watch('cache-image'):
@@ -688,21 +687,21 @@ class Builder(object): # pragma: no cover
(str(blob), type(blob)))
builder = klass(blob, self.factory, self.app.settings, self.cachedir,
- self.get_cache_id(blob), self.tempdir,
+ self.get_cache_id(blob.morph), self.tempdir,
self.app.clean_env())
builder.real_msg = self.msg
builder.dump_memory_profile = self.dump_memory_profile
return builder
- def get_cache_id(self, blob):
- logging.debug('get_cache_id(%s)' % blob)
+ def get_cache_id(self, morph):
+ logging.debug('get_cache_id(%s)' % morph)
- if blob.morph.kind == 'chunk':
+ if morph.kind == 'chunk':
kids = []
- elif blob.morph.kind == 'stratum':
+ elif morph.kind == 'stratum':
kids = []
- for source in blob.morph.sources:
+ for source in morph.sources:
repo = source['repo']
ref = source['ref']
treeish = self.source_manager.get_treeish(repo, ref)
@@ -710,26 +709,24 @@ class Builder(object): # pragma: no cover
if 'morph' in source
else source['name'])
filename = '%s.morph' % filename
- morph = self.morph_loader.load(treeish, filename)
- chunk = morphlib.blobs.Blob.create_blob(morph)
+ chunk = self.morph_loader.load(treeish, filename)
cache_id = self.get_cache_id(chunk)
kids.append(cache_id)
- elif blob.morph.kind == 'system':
+ elif morph.kind == 'system':
kids = []
- for stratum_name in blob.morph.strata:
+ for stratum_name in morph.strata:
filename = '%s.morph' % stratum_name
- morph = self.morph_loader.load(blob.morph.treeish, filename)
- stratum = morphlib.blobs.Blob.create_blob(morph)
+ stratum = self.morph_loader.load(morph.treeish, filename)
cache_id = self.get_cache_id(stratum)
kids.append(cache_id)
else:
raise NotImplementedError('unknown morph kind %s' %
- blob.morph.kind)
+ morph.kind)
dict_key = {
- 'filename': blob.morph.filename,
+ 'filename': morph.filename,
'arch': morphlib.util.arch(),
- 'ref': blob.morph.treeish.sha1,
+ 'ref': morph.treeish.sha1,
'kids': ''.join(self.cachedir.key(k) for k in kids),
'env': self.build_env,
}
diff --git a/morphlib/fsutils.py b/morphlib/fsutils.py
new file mode 100644
index 00000000..dbe8b261
--- /dev/null
+++ b/morphlib/fsutils.py
@@ -0,0 +1,72 @@
+# 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.
+
+import os
+
+
+def create_image(ex, image_name, size):
+ # FIXME a pure python implementation may be better
+ ex.runv(['dd', 'if=/dev/zero', 'of=' + image_name, 'bs=1',
+ 'seek=%d' % size, 'count=0'])
+
+def partition_image(ex, image_name):
+ # FIXME make this more flexible with partitioning options
+ ex.runv(['sfdisk', image_name], feed_stdin='1,,83,*\n')
+
+def install_mbr(ex, image_name):
+ for path in ['/usr/lib/extlinux/mbr.bin',
+ '/usr/share/syslinux/mbr.bin']:
+ if os.path.exists(path):
+ ex.runv(['dd', 'if=' + path, 'of=' + image_name,
+ 'conv=notrunc'])
+ break
+
+def setup_device_mapping(ex, image_name):
+ out = ex.runv(['sfdisk', '-d', image_name])
+ for line in out.splitlines():
+ words = line.split()
+ if (len(words) >= 4 and
+ words[2] == 'start=' and
+ words[3] != '0,'):
+ n = int(words[3][:-1]) # skip trailing comma
+ start = n * 512
+ break
+
+ ex.runv(['losetup', '-o', str(start), '-f', image_name])
+
+ out = ex.runv(['losetup', '-j', image_name])
+ line = out.strip()
+ i = line.find(':')
+ return line[:i]
+
+def create_fs(ex, partition):
+ # FIXME: the hardcoded size of 4GB is icky but the default broke
+ # when we used mkfs -t ext4
+ ex.runv(['mkfs', '-t', 'btrfs', '-L', 'baserock',
+ '-b', '4294967296', partition])
+
+def mount(ex, partition, mount_point):
+ os.mkdir(mount_point)
+ ex.runv(['mount', partition, mount_point])
+
+def unmount(ex, mount_point):
+ ex.runv(['umount', mount_point])
+
+def undo_device_mapping(ex, image_name):
+ out = ex.runv(['losetup', '-j', image_name])
+ for line in out.splitlines():
+ i = line.find(':')
+ device = line[:i]
+ ex.runv(['losetup', '-d', device])
diff --git a/tests.as-root/hello-chunk.tar.gz b/tests.as-root/hello-chunk.tar.gz
new file mode 100644
index 00000000..91e27347
--- /dev/null
+++ b/tests.as-root/hello-chunk.tar.gz
Binary files differ
diff --git a/tests.as-root/hello-stratum.morph b/tests.as-root/hello-stratum.morph
new file mode 100644
index 00000000..11ac4e1c
--- /dev/null
+++ b/tests.as-root/hello-stratum.morph
@@ -0,0 +1,10 @@
+{
+ "name": "hello",
+ "kind": "stratum",
+ "sources": {
+ "hello": {
+ "repo": "hello",
+ "ref": "master"
+ }
+ }
+}
diff --git a/tests.as-root/make-patch.script b/tests.as-root/make-patch.script
new file mode 100755
index 00000000..cc2af3b6
--- /dev/null
+++ b/tests.as-root/make-patch.script
@@ -0,0 +1,86 @@
+#!/bin/sh
+#
+# Test making a patch between two different system images
+#
+# 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.
+
+set -eu
+
+( # make the dummy stratum contain a chunk for a fake kernel
+ mkdir -p "$DATADIR/dummykernel"
+ cd "$DATADIR/dummykernel"
+ git init --quiet
+ cat <<EOF >dummykernel.morph
+{
+ "name": "dummykernel",
+ "kind": "chunk",
+ "install-commands": [
+ "mkdir -p \"\$DESTDIR/boot\"",
+ "touch \"\$DESTDIR\"/extlinux.conf",
+ "touch \"\$DESTDIR\"/boot/vmlinuz",
+ "touch \"\$DESTDIR\"/boot/System.map"
+ ]
+}
+EOF
+
+
+ git add .
+ git commit --quiet -m "Make dummy boot files"
+
+ cd "$DATADIR/morphs-repo"
+ git checkout --quiet master
+ sed -i -e 's/^.*sources.*$/&\
+ {\
+ "name": "dummykernel",\
+ "ref": "master"\
+ },\
+/' hello-stratum.morph
+
+ git commit --quiet -m "add dummy kernel" hello-stratum.morph
+)
+
+tests/morph build morphs-repo master hello-system.morph
+
+# save the stratum as we will apply the patch on top of this later
+cp "$DATADIR/cache/"*stratum* "$DATADIR"/farrokh-stratum
+
+
+( # make an evil stratum
+ cd "$DATADIR/chunk-repo"
+ git checkout --quiet -b evil farrokh
+ sed -i -e 's/hello/goodbye/g' hello.c
+ git add hello.c
+ git commit --quiet -m "Make the program evil"
+
+ cd "$DATADIR/morphs-repo"
+ git checkout --quiet -b evil master
+ sed -i -e 's/farrokh/evil/g' hello-stratum.morph
+ git add hello-stratum.morph
+ git commit --quiet -m "Build evil systems"
+)
+
+tests/morph build morphs-repo evil hello-system.morph
+
+# make a patch to make the system evil
+PATCH="$DATADIR"/patchfile
+tests/morph make-patch "$PATCH" morphs-repo master hello-system.morph \
+ morphs-repo evil hello-system.morph
+
+UNPACKED="$DATADIR"/unpacked
+mkdir -p "$UNPACKED"
+tar -C "$UNPACKED" -xf "$DATADIR"/farrokh-stratum
+(cd "$UNPACKED" && tbdiff-deploy "$PATCH")
+"$UNPACKED"/bin/hello
diff --git a/tests.as-root/make-patch.stderr b/tests.as-root/make-patch.stderr
new file mode 100644
index 00000000..9ed08116
--- /dev/null
+++ b/tests.as-root/make-patch.stderr
@@ -0,0 +1,16 @@
+cmd_dir_delta baserock
+cmd_dir_enter baserock
+cmd_metadata_update hello-stratum.meta
+cmd_metadata_update hello.meta
+cmd_dir_leave
+cmd_dir_delta etc
+cmd_dir_enter etc
+cmd_metadata_update fstab
+cmd_dir_leave
+cmd_metadata_update extlinux.conf
+cmd_dir_enter boot
+cmd_dir_leave
+cmd_dir_delta bin
+cmd_dir_enter bin
+cmd_file_delta hello
+cmd_dir_leave
diff --git a/tests.as-root/make-patch.stdout b/tests.as-root/make-patch.stdout
new file mode 100644
index 00000000..e8bb667c
--- /dev/null
+++ b/tests.as-root/make-patch.stdout
@@ -0,0 +1,3 @@
+morphs-repo master hello-system.morph
+morphs-repo evil hello-system.morph
+goodbye, world
diff --git a/tests.as-root/morph b/tests.as-root/morph
new file mode 100755
index 00000000..8ba3ae78
--- /dev/null
+++ b/tests.as-root/morph
@@ -0,0 +1,29 @@
+#!/bin/sh
+#
+# Run morph in a way suitable for tests.
+#
+# 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.
+
+set -eu
+
+fatalcat(){
+ cat "$1" 1>&2
+ return 1
+}
+
+PATH="$(pwd):$PATH"
+./morph --no-default-config --config="$DATADIR/morph.conf" "$@" || \
+ fatalcat "$DATADIR/morph.log"
diff --git a/tests.as-root/setup b/tests.as-root/setup
new file mode 100755
index 00000000..004c0ceb
--- /dev/null
+++ b/tests.as-root/setup
@@ -0,0 +1,128 @@
+#!/bin/sh
+#
+# Create git repositories for tests. The chunk repository will contain a
+# simple "hello, world" C program, and two branches ("master", "farrokh"),
+# with the master branch containing just a README. The two branches are there
+# so that we can test building a branch that hasn't been checked out.
+# The branches are different so that we know that if the wrong branch
+# is uses, the build will fail.
+#
+# The stratum repository contains a single branch, "master", with a
+# stratum and a system morphology that include the chunk above.
+#
+# Copyright (C) 2011, 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.
+
+
+set -eu
+
+# The $DATADIR should be empty at the beginnig of each test.
+find "$DATADIR" -mindepth 1 -delete
+
+# Create chunk repository.
+
+chunkrepo="$DATADIR/chunk-repo"
+mkdir "$chunkrepo"
+cd "$chunkrepo"
+git init --quiet
+
+cat <<EOF > README
+This is a sample README.
+EOF
+git add README
+git commit --quiet -m "add README"
+
+git checkout --quiet -b farrokh
+
+cat <<EOF > hello.c
+#include <stdio.h>
+int main(void)
+{
+ puts("hello, world");
+ return 0;
+}
+EOF
+git add hello.c
+
+cat <<EOF > hello.morph
+{
+ "name": "hello",
+ "kind": "chunk",
+ "build-system": "dummy",
+ "build-commands": [
+ "gcc -o hello hello.c"
+ ],
+ "install-commands": [
+ "install -d \\"\$DESTDIR\\"/etc",
+ "install -d \\"\$DESTDIR\\"/bin",
+ "install hello \\"\$DESTDIR\\"/bin/hello"
+ ]
+}
+EOF
+git add hello.morph
+
+git commit --quiet -m "add a hello world program and morph"
+
+git checkout --quiet master
+
+
+
+# Create morph repository.
+
+morphsrepo="$DATADIR/morphs-repo"
+mkdir "$morphsrepo"
+cd "$morphsrepo"
+git init --quiet
+
+cat <<EOF > hello-stratum.morph
+{
+ "name": "hello-stratum",
+ "kind": "stratum",
+ "sources": [
+ {
+ "name": "hello",
+ "repo": "chunk-repo",
+ "ref": "farrokh"
+ }
+ ]
+}
+EOF
+git add hello-stratum.morph
+
+cat <<EOF > hello-system.morph
+{
+ "name": "hello-system",
+ "kind": "system",
+ "disk-size": "1G",
+ "strata": [
+ "hello-stratum"
+ ]
+}
+EOF
+git add hello-system.morph
+
+git commit --quiet -m "add morphs"
+
+
+# Create a morph configuration file.
+cat <<EOF > "$DATADIR/morph.conf"
+[config]
+git-base-url = file://$DATADIR/
+cachedir = $DATADIR/cache
+log = $DATADIR/morph.log
+keep-path = true
+no-distcc = true
+EOF
+
diff --git a/tests/morph b/tests/morph
index 3554a2c8..8ba3ae78 100755
--- a/tests/morph
+++ b/tests/morph
@@ -19,6 +19,11 @@
set -eu
+fatalcat(){
+ cat "$1" 1>&2
+ return 1
+}
+
PATH="$(pwd):$PATH"
./morph --no-default-config --config="$DATADIR/morph.conf" "$@" || \
- cat "$DATADIR/morph.log" 1>&2
+ fatalcat "$DATADIR/morph.log"
diff --git a/without-test-modules b/without-test-modules
index 114afa1a..75070b46 100644
--- a/without-test-modules
+++ b/without-test-modules
@@ -2,3 +2,4 @@ morphlib/__init__.py
morphlib/builddependencygraph.py
morphlib/tester.py
morphlib/git.py
+morphlib/fsutils.py