diff options
-rwxr-xr-x | check | 4 | ||||
-rwxr-xr-x | morph | 47 | ||||
-rw-r--r-- | morphlib/__init__.py | 1 | ||||
-rw-r--r-- | morphlib/builder.py | 147 | ||||
-rw-r--r-- | morphlib/fsutils.py | 72 | ||||
-rw-r--r-- | tests.as-root/hello-chunk.tar.gz | bin | 0 -> 12596 bytes | |||
-rw-r--r-- | tests.as-root/hello-stratum.morph | 10 | ||||
-rwxr-xr-x | tests.as-root/make-patch.script | 86 | ||||
-rw-r--r-- | tests.as-root/make-patch.stderr | 16 | ||||
-rw-r--r-- | tests.as-root/make-patch.stdout | 3 | ||||
-rwxr-xr-x | tests.as-root/morph | 29 | ||||
-rwxr-xr-x | tests.as-root/setup | 128 | ||||
-rwxr-xr-x | tests/morph | 7 | ||||
-rw-r--r-- | without-test-modules | 1 |
14 files changed, 475 insertions, 76 deletions
@@ -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 @@ -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 Binary files differnew file mode 100644 index 00000000..91e27347 --- /dev/null +++ b/tests.as-root/hello-chunk.tar.gz 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 |