summaryrefslogtreecommitdiff
path: root/morphlib
diff options
context:
space:
mode:
authorLars Wirzenius <liw@liw.fi>2011-09-29 10:51:04 +0100
committerLars Wirzenius <liw@liw.fi>2011-09-29 10:51:04 +0100
commit50a948a728a0768907700befe501bb743828b67b (patch)
treea4cbc074215f6b26b8577b85c3538392e0bee42b /morphlib
downloadmorph-50a948a728a0768907700befe501bb743828b67b.tar.gz
Initial import.
Diffstat (limited to 'morphlib')
-rw-r--r--morphlib/__init__.py27
-rw-r--r--morphlib/builder.py117
-rw-r--r--morphlib/execute.py87
-rw-r--r--morphlib/execute_tests.py51
-rw-r--r--morphlib/morphology.py196
-rw-r--r--morphlib/morphology_tests.py458
-rw-r--r--morphlib/tempdir.py58
-rw-r--r--morphlib/tempdir_tests.py69
-rw-r--r--morphlib/util.py32
-rw-r--r--morphlib/util_tests.py36
10 files changed, 1131 insertions, 0 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
new file mode 100644
index 00000000..bd318978
--- /dev/null
+++ b/morphlib/__init__.py
@@ -0,0 +1,27 @@
+# Copyright (C) 2011 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; either 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.
+
+
+'''Baserock library.'''
+
+
+__version__ = '0.0'
+
+
+import builder
+import execute
+import morphology
+import tempdir
+import util
diff --git a/morphlib/builder.py b/morphlib/builder.py
new file mode 100644
index 00000000..befe8485
--- /dev/null
+++ b/morphlib/builder.py
@@ -0,0 +1,117 @@
+# Copyright (C) 2011 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; either 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 logging
+import os
+
+import morphlib
+
+
+class Builder(object):
+
+ '''Build binary objects for Baserock.
+
+ The objects may be chunks or strata.'''
+
+ def __init__(self, tempdir, msg):
+ self.tempdir = tempdir
+ self.msg = msg
+
+ def build(self, morph):
+ '''Build a binary based on a morphology.'''
+ if morph.kind == 'chunk':
+ self.build_chunk(morph)
+ elif morph.kind == 'stratum':
+ self.build_stratum(morph)
+ else:
+ raise Exception('Unknown kind of morphology: %s' % morph.kind)
+
+ def build_chunk(self, morph):
+ '''Build a chunk from a morphology.'''
+ logging.debug('Building chunk')
+ self.ex = morphlib.execute.Execute(self._build, self.msg)
+ self.ex.env['WORKAREA'] = self.tempdir.dirname
+ self.ex.env['DESTDIR'] = self._inst + '/'
+ self.create_build_tree(morph)
+ self.ex.run(morph.configure_commands)
+ self.ex.run(morph.build_commands)
+ self.ex.run(morph.test_commands)
+ self.ex.run(morph.install_commands)
+ self.create_chunk(morph)
+ self.tempdir.clear()
+
+ def create_build_tree(self, morph):
+ '''Export sources from git into the ``self._build`` directory.'''
+
+ logging.debug('Creating build tree at %s' % self._build)
+ tarball = self.tempdir.join('sources.tar')
+ self.ex.runv(['git', 'archive',
+ '--output', tarball,
+ '--remote', morph.source['repo'],
+ morph.source['ref']])
+ os.mkdir(self._build)
+ self.ex.runv(['tar', '-C', self._build, '-xf', tarball])
+ os.remove(tarball)
+
+ def create_chunk(self, morph):
+ '''Create a Baserock chunk from the ``self._inst`` directory.
+
+ The directory must be filled in with all the relevant files already.
+
+ '''
+
+ dirname = os.path.dirname(morph.filename)
+ filename = os.path.join(dirname, '%s.chunk' % morph.name)
+ logging.debug('Creating chunk %s at %s' % (morph.name, filename))
+ self.ex.runv(['tar', '-C', self._inst, '-czf', filename, '.'])
+
+ def build_stratum(self, morph):
+ '''Build a stratum from a morphology.'''
+ os.mkdir(self._inst)
+ self.ex = morphlib.execute.Execute(self.tempdir.dirname, self.msg)
+ for chunk_name in morph.sources:
+ filename = self._chunk_filename(morph, chunk_name)
+ self.unpack_chunk(filename)
+ self.create_stratum(morph)
+ self.tempdir.clear()
+
+ def unpack_chunk(self, filename):
+ self.ex.runv(['tar', '-C', self._inst, '-xf', filename])
+
+ def create_stratum(self, morph):
+ '''Create a Baserock stratum from the ``self._inst`` directory.
+
+ The directory must be filled in with all the relevant files already.
+
+ '''
+
+ dirname = os.path.dirname(morph.filename)
+ filename = os.path.join(dirname, '%s.stratum' % morph.name)
+ logging.debug('Creating stratum %s at %s' % (morph.name, filename))
+ self.ex.runv(['tar', '-C', self._inst, '-czf', filename, '.'])
+
+ @property
+ def _build(self):
+ return self.tempdir.join('build')
+
+ @property
+ def _inst(self):
+ return self.tempdir.join('inst')
+
+ def _chunk_filename(self, morph, chunk_name):
+ dirname = os.path.dirname(morph.filename)
+ return os.path.join(dirname, '%s.chunk' % chunk_name)
+
diff --git a/morphlib/execute.py b/morphlib/execute.py
new file mode 100644
index 00000000..9a279591
--- /dev/null
+++ b/morphlib/execute.py
@@ -0,0 +1,87 @@
+# Copyright (C) 2011 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; either 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 logging
+import os
+import subprocess
+
+import morphlib
+
+
+class CommandFailure(Exception):
+
+ pass
+
+
+class Execute(object):
+
+ '''Execute commands for morph.'''
+
+ def __init__(self, dirname, msg):
+ self._setup_env()
+ self.dirname = dirname
+ self.msg = msg
+
+ def _setup_env(self):
+ self.env = dict(os.environ)
+
+ def run(self, commands):
+ '''Execute a list of commands.
+
+ If a command fails (returns non-zero exit code), the rest are
+ not run, and CommandFailure is returned.
+
+ '''
+
+ stdouts = []
+ for command in commands:
+ self.msg('# %s' % command)
+ p = subprocess.Popen([command], shell=True,
+ stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE,
+ env=self.env,
+ cwd=self.dirname)
+ out, err = p.communicate()
+ logging.debug('Exit code: %d' % p.returncode)
+ logging.debug('Standard output:\n%s' % morphlib.util.indent(out))
+ logging.debug('Standard error:\n%s' % morphlib.util.indent(err))
+ if p.returncode != 0:
+ raise CommandFailure('Command failed: %s\n%s' %
+ (command, morphlib.util.indent(err)))
+ stdouts.append(out)
+ return stdouts
+
+ def runv(self, argv):
+ '''Run a command given as a list of argv elements.
+
+ Return standard output. Raise ``CommandFailure`` if the command
+ fails. Log standard output and error in any case.
+
+ '''
+
+ self.msg('# %s' % ' '.join(argv))
+ p = subprocess.Popen(argv, stdout=subprocess.PIPE,
+ stderr=subprocess.PIPE)
+ out, err = p.communicate()
+
+ logging.debug('Exit code: %d' % p.returncode)
+ logging.debug('Standard output:\n%s' % morphlib.util.indent(out))
+ logging.debug('Standard error:\n%s' % morphlib.util.indent(err))
+ if p.returncode != 0:
+ raise CommandFailure('Command failed: %s\n%s' %
+ (argv, morphlib.util.indent(err)))
+ return out
+
diff --git a/morphlib/execute_tests.py b/morphlib/execute_tests.py
new file mode 100644
index 00000000..721d0807
--- /dev/null
+++ b/morphlib/execute_tests.py
@@ -0,0 +1,51 @@
+# Copyright (C) 2011 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; either 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
+import unittest
+
+import morphlib
+
+
+class ExecuteTests(unittest.TestCase):
+
+ def setUp(self):
+ self.e = morphlib.execute.Execute('/')
+
+ def test_has_same_path_as_environment(self):
+ self.assertEqual(self.e.env['PATH'], os.environ['PATH'])
+
+ def test_executes_true_ok(self):
+ self.assertEqual(self.e.run(['true']), [''])
+
+ def test_raises_commandfailure_for_false(self):
+ self.assertRaises(morphlib.execute.CommandFailure,
+ self.e.run, ['false'])
+
+ def test_returns_stdout_from_all_commands(self):
+ self.assertEqual(self.e.run(['echo foo', 'echo bar']),
+ ['foo\n', 'bar\n'])
+
+ def test_sets_working_directory(self):
+ self.assertEqual(self.e.run(['pwd']), ['/\n'])
+
+ def test_executes_argv(self):
+ self.assertEqual(self.e.runv(['echo', 'foo']), 'foo\n')
+
+ def test_raises_error_when_argv_fails(self):
+ self.assertRaises(morphlib.execute.CommandFailure,
+ self.e.runv, ['false'])
+
diff --git a/morphlib/morphology.py b/morphlib/morphology.py
new file mode 100644
index 00000000..d31a000c
--- /dev/null
+++ b/morphlib/morphology.py
@@ -0,0 +1,196 @@
+# Copyright (C) 2011 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; either 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 json
+import logging
+
+
+class SchemaError(Exception):
+
+ pass
+
+
+class Morphology(object):
+
+ '''Represent a morphology: description of how to build binaries.'''
+
+ def __init__(self, fp, baseurl=None):
+ self._fp = fp
+ self._baseurl = baseurl or ''
+ self._load()
+
+ def _load(self):
+ logging.debug('Loading morphology %s' % self._fp.name)
+ self._dict = json.load(self._fp)
+
+ if 'name' not in self._dict:
+ raise self._error('must contain "name"')
+
+ if not self.name:
+ raise self._error('"name" must not be empty')
+
+ if 'kind' not in self._dict:
+ raise self._error('must contain "kind"')
+
+ if self.kind == 'chunk':
+ self._validate_chunk()
+ self.source['repo'] = self._join_with_baseurl(self.source['repo'])
+ elif self.kind == 'stratum':
+ self._validate_stratum()
+ for source in self.sources.itervalues():
+ source['repo'] = self._join_with_baseurl(source['repo'])
+ else:
+ raise self._error('kind must be chunk or stratum, not %s' %
+ self.kind)
+
+ self.filename = self._fp.name
+
+ def _validate_chunk(self):
+ valid_toplevel_keys = ['name', 'kind', 'source', 'configure-commands',
+ 'build-commands', 'test-commands',
+ 'install-commands']
+
+ if 'source' not in self._dict:
+ raise self._error('chunks must have "source" field')
+
+ if type(self.source) != dict:
+ raise self._error('"source" must be a dictionary')
+
+ if len(self.source) == 0:
+ raise self._error('"source" must not be empty')
+
+ if 'repo' not in self.source:
+ raise self._error('"source" must contain "repo"')
+
+ if not self.source['repo']:
+ raise self._error('"source" must contain non-empty "repo"')
+
+ if 'ref' not in self.source:
+ raise self._error('"source" must contain "ref"')
+
+ if not self.source['ref']:
+ raise self._error('"source" must contain non-empty "ref"')
+
+ for key in self.source.keys():
+ if key not in ('repo', 'ref'):
+ raise self._error('unknown key "%s" in "source"' % key)
+
+ cmdlists = [
+ (self.configure_commands, 'configure-commands'),
+ (self.build_commands, 'build-commands'),
+ (self.test_commands, 'test-commands'),
+ (self.install_commands, 'install-commands'),
+ ]
+ for value, name in cmdlists:
+ if type(value) != list:
+ raise self._error('"%s" must be a list' % name)
+ for x in value:
+ if type(x) != unicode:
+ raise self._error('"%s" must contain strings' % name)
+
+ for key in self._dict.keys():
+ if key not in valid_toplevel_keys:
+ raise self._error('unknown key "%s"' % key)
+
+ def _validate_stratum(self):
+ valid_toplevel_keys = ['name', 'kind', 'sources']
+
+ if 'sources' not in self._dict:
+ raise self._error('stratum must contain "sources"')
+
+ if type(self.sources) != dict:
+ raise self._error('"sources" must be a dict')
+
+ if len(self.sources) == 0:
+ raise self._error('"sources" must not be empty')
+
+ for name, source in self.sources.iteritems():
+ if type(source) != dict:
+ raise self._error('"sources" must contain dicts')
+ if 'repo' not in source:
+ raise self._error('sources must have "repo"')
+ if type(source['repo']) != unicode:
+ raise self._error('"repo" must be a string')
+ if not source['repo']:
+ raise self._error('"repo" must be a non-empty string')
+ if 'ref' not in source:
+ raise self._error('sources must have "ref"')
+ if type(source['ref']) != unicode:
+ raise self._error('"ref" must be a string')
+ if not source['ref']:
+ raise self._error('"ref" must be a non-empty string')
+ for key in source:
+ if key not in ('repo', 'ref'):
+ raise self._error('unknown key "%s" in sources' % key)
+
+ for key in self._dict.keys():
+ if key not in valid_toplevel_keys:
+ raise self._error('unknown key "%s"' % key)
+
+ @property
+ def name(self):
+ return self._dict['name']
+
+ @property
+ def kind(self):
+ return self._dict['kind']
+
+ @property
+ def source(self):
+ return self._dict['source']
+
+ @property
+ def sources(self):
+ return self._dict['sources']
+
+ @property
+ def configure_commands(self):
+ return self._dict.get('configure-commands', [])
+
+ @property
+ def build_commands(self):
+ return self._dict.get('build-commands', [])
+
+ @property
+ def test_commands(self):
+ return self._dict.get('test-commands', [])
+
+ @property
+ def install_commands(self):
+ return self._dict.get('install-commands', [])
+
+ @property
+ def manifest(self):
+ if self.kind == 'chunk':
+ return [(self.source['repo'], self.source['ref'])]
+ else:
+ return [(source['repo'], source['ref'])
+ for source in self.sources.itervalues()]
+
+ def _join_with_baseurl(self, url):
+ is_relative = (':' not in url or
+ '/' not in url or
+ url.find('/') < url.find(':'))
+ if is_relative:
+ if not url.endswith('/'):
+ url += '/'
+ return self._baseurl + url
+ else:
+ return url
+
+ def _error(self, msg):
+ return SchemaError('Morphology %s: %s' % (self._fp.name, msg))
+
diff --git a/morphlib/morphology_tests.py b/morphlib/morphology_tests.py
new file mode 100644
index 00000000..7a38bff4
--- /dev/null
+++ b/morphlib/morphology_tests.py
@@ -0,0 +1,458 @@
+# Copyright (C) 2011 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; either 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 json
+import StringIO
+import unittest
+
+import morphlib
+
+
+class MockFile(StringIO.StringIO):
+
+ def __init__(self, *args, **kwargs):
+ StringIO.StringIO.__init__(self, *args, **kwargs)
+ self.name = 'mockfile'
+
+
+class MorphologyTests(unittest.TestCase):
+
+ def assertRaisesSchemaError(self, morph_dict):
+ f = MockFile(json.dumps(morph_dict))
+ self.assertRaises(morphlib.morphology.SchemaError,
+ morphlib.morphology.Morphology, f)
+
+ def test_raises_exception_for_empty_file(self):
+ self.assertRaises(ValueError,
+ morphlib.morphology.Morphology,
+ MockFile())
+
+ def test_raises_exception_for_file_without_kind_field(self):
+ self.assertRaisesSchemaError({})
+
+ def test_raises_exception_for_chunk_with_unknown_keys_only(self):
+ self.assertRaisesSchemaError({ 'x': 'y' })
+
+ def test_raises_exception_if_name_only(self):
+ self.assertRaisesSchemaError({ 'name': 'hello' })
+
+ def test_raises_exception_if_name_is_empty(self):
+ self.assertRaisesSchemaError({ 'name': '', 'kind': 'chunk',
+ 'sources': { 'repo': 'x', 'ref': 'y' }})
+
+ def test_raises_exception_if_kind_only(self):
+ self.assertRaisesSchemaError({ 'kind': 'chunk' })
+
+ def test_raises_exception_for_kind_that_has_unknown_kind(self):
+ self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'x' })
+
+ def test_raises_exception_for_chunk_without_source(self):
+ self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'chunk' })
+
+ def test_raises_exception_for_chunk_with_nondict_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': [],
+ })
+
+ def test_raises_exception_for_chunk_with_empty_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {},
+ })
+
+ def test_raises_exception_for_chunk_without_repo_in_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'x': 'y'
+ },
+ })
+
+ def test_raises_exception_for_chunk_with_empty_repo_in_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': '',
+ 'ref': 'master'
+ },
+ })
+
+ def test_raises_exception_for_chunk_without_ref_in_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ },
+ })
+
+ def test_raises_exception_for_chunk_with_empty_ref_in_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': ''
+ },
+ })
+
+ def test_raises_exception_for_chunk_with_unknown_keys_in_source(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master',
+ 'x': 'y'
+ },
+ })
+
+ def test_raises_exception_for_chunk_with_unknown_keys(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'x': 'y'
+ })
+
+ def test_raises_exception_for_nonlist_configure_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'configure-commands': 0,
+ })
+
+ def test_raises_exception_for_list_of_nonstring_configure_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'configure-commands': [0],
+ })
+
+ def test_raises_exception_for_nonlist_build_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'build-commands': 0,
+ })
+
+ def test_raises_exception_for_list_of_nonstring_build_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'build-commands': [0],
+ })
+
+ def test_raises_exception_for_nonlist_test_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'test-commands': 0,
+ })
+
+ def test_raises_exception_for_list_of_nonstring_test_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'build-commands': [0],
+ })
+
+ def test_raises_exception_for_nonlist_install_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'install-commands': 0,
+ })
+
+ def test_raises_exception_for_list_of_nonstring_install_commands(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'chunk',
+ 'source': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ },
+ 'install-commands': [0],
+ })
+
+ def test_accepts_valid_chunk_morphology(self):
+ chunk = morphlib.morphology.Morphology(
+ MockFile('''
+ {
+ "name": "hello",
+ "kind": "chunk",
+ "source":
+ {
+ "repo": "foo",
+ "ref": "ref"
+ },
+ "configure-commands": ["./configure"],
+ "build-commands": ["make"],
+ "test-commands": ["make check"],
+ "install-commands": ["make install"]
+ }'''))
+ self.assertEqual(chunk.kind, 'chunk')
+ self.assertEqual(chunk.filename, 'mockfile')
+
+ def test_raises_exception_for_stratum_without_sources(self):
+ self.assertRaisesSchemaError({ 'name': 'hello', 'kind': 'stratum' })
+
+ def test_raises_exception_for_stratum_with_nondict_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': [],
+ })
+
+ def test_raises_exception_for_stratum_with_empty_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {},
+ })
+
+ def test_raises_exception_for_stratum_with_bad_children_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': 0,
+ },
+ })
+
+ def test_raises_exception_for_stratum_without_repo_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'ref': 'master'
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_empty_repo_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': '',
+ 'ref': 'master'
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_nonstring_repo_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 0,
+ 'ref': 'master'
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_without_ref_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 'foo',
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_empty_ref_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 'foo',
+ 'ref': ''
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_nonstring_ref_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 'foo',
+ 'ref': 0
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_unknown_keys_in_sources(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 'foo',
+ 'ref': 'master',
+ 'x': 'y'
+ }
+ },
+ })
+
+ def test_raises_exception_for_stratum_with_unknown_keys(self):
+ self.assertRaisesSchemaError({
+ 'name': 'hello',
+ 'kind': 'stratum',
+ 'sources': {
+ 'foo': {
+ 'repo': 'foo',
+ 'ref': 'master'
+ }
+ },
+ 'x': 'y'
+ })
+
+ def test_accepts_valid_stratum_morphology(self):
+ morph = morphlib.morphology.Morphology(
+ MockFile('''
+ {
+ "name": "hello",
+ "kind": "stratum",
+ "sources":
+ {
+ "foo": {
+ "repo": "foo",
+ "ref": "ref"
+ }
+ }
+ }'''))
+ self.assertEqual(morph.kind, 'stratum')
+ self.assertEqual(morph.filename, 'mockfile')
+
+
+class ChunkRepoTests(unittest.TestCase):
+
+ def chunk(self, repo):
+ return morphlib.morphology.Morphology(
+ MockFile('''
+ {
+ "name": "hello",
+ "kind": "chunk",
+ "source":
+ {
+ "repo": "%s",
+ "ref": "HEAD"
+ },
+ "configure-commands": ["./configure"],
+ "build-commands": ["make"],
+ "test-commands": ["make check"],
+ "install-commands": ["make install"]
+ }''' % repo),
+ baseurl='git://git.baserock.org/')
+
+ def test_returns_repo_with_schema_as_is(self):
+ self.assertEqual(self.chunk('git://git.baserock.org/foo/').manifest,
+ [('git://git.baserock.org/foo/', 'HEAD')])
+
+ def test_prepends_baseurl_to_repo_without_schema(self):
+ self.assertEqual(self.chunk('foo').manifest,
+ [('git://git.baserock.org/foo/', 'HEAD')])
+
+ def test_leaves_absolute_repo_in_source_dict_as_is(self):
+ chunk = self.chunk('git://git.baserock.org/foo/')
+ self.assertEqual(chunk.source['repo'], 'git://git.baserock.org/foo/')
+
+ def test_makes_relative_repo_url_absolute_in_source_dict(self):
+ chunk = self.chunk('foo')
+ self.assertEqual(chunk.source['repo'], 'git://git.baserock.org/foo/')
+
+
+class StratumRepoTests(unittest.TestCase):
+
+ def stratum(self, repo):
+ return morphlib.morphology.Morphology(
+ MockFile('''
+ {
+ "name": "hello",
+ "kind": "stratum",
+ "sources":
+ {
+ "foo": {
+ "repo": "%s",
+ "ref": "HEAD"
+ }
+ }
+ }''' % repo),
+ baseurl='git://git.baserock.org/')
+
+ def test_returns_repo_with_schema_as_is(self):
+ self.assertEqual(self.stratum('git://git.baserock.org/foo/').manifest,
+ [('git://git.baserock.org/foo/', 'HEAD')])
+
+ def test_prepends_baseurl_to_repo_without_schema(self):
+ self.assertEqual(self.stratum('foo').manifest,
+ [('git://git.baserock.org/foo/', 'HEAD')])
+
+ def test_leaves_absolute_repo_in_source_dict_as_is(self):
+ stratum = self.stratum('git://git.baserock.org/foo/')
+ self.assertEqual(stratum.sources['foo']['repo'],
+ 'git://git.baserock.org/foo/')
+
+ def test_makes_relative_repo_url_absolute_in_source_dict(self):
+ stratum = self.stratum('foo')
+ self.assertEqual(stratum.sources['foo']['repo'],
+ 'git://git.baserock.org/foo/')
+
diff --git a/morphlib/tempdir.py b/morphlib/tempdir.py
new file mode 100644
index 00000000..7f58cdc3
--- /dev/null
+++ b/morphlib/tempdir.py
@@ -0,0 +1,58 @@
+# Copyright (C) 2011 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; either 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 logging
+import os
+import shutil
+import tempfile
+
+
+class Tempdir(object):
+
+ '''Temporary file handling for morph.'''
+
+ def __init__(self, parent=None):
+ self.dirname = tempfile.mkdtemp(dir=parent)
+ logging.debug('Created temporary directory %s' % self.dirname)
+
+ def remove(self):
+ '''Remove the temporary directory.'''
+ logging.debug('Removing temporary directory %s' % self.dirname)
+ shutil.rmtree(self.dirname)
+ self.dirname = None
+
+ def clear(self):
+ '''Clear temporary directory of everything.'''
+ for x in os.listdir(self.dirname):
+ filename = self.join(x)
+ if os.path.isdir(filename):
+ shutil.rmtree(filename)
+ else:
+ os.remove(filename)
+
+ def join(self, relative):
+ '''Return full path to file in temporary directory.
+
+ The relative path is given appended to the name of the
+ temporary directory. If the relative path is actually absolute,
+ it is forced to become relative.
+
+ The returned path is normalized.
+
+ '''
+
+ return os.path.normpath(os.path.join(self.dirname, './' + relative))
+
diff --git a/morphlib/tempdir_tests.py b/morphlib/tempdir_tests.py
new file mode 100644
index 00000000..6572b70a
--- /dev/null
+++ b/morphlib/tempdir_tests.py
@@ -0,0 +1,69 @@
+# Copyright (C) 2011 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; either 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
+import shutil
+import unittest
+
+import morphlib
+
+
+class TempdirTests(unittest.TestCase):
+
+ def setUp(self):
+ self.parent = os.path.abspath('unittest-tempdir')
+ os.mkdir(self.parent)
+ self.tempdir = morphlib.tempdir.Tempdir(parent=self.parent)
+
+ def tearDown(self):
+ shutil.rmtree(self.parent)
+
+ def test_creates_the_directory(self):
+ self.assert_(os.path.isdir(self.tempdir.dirname))
+
+ def test_creates_subdirectory_of_parent(self):
+ self.assert_(self.tempdir.dirname.startswith(self.parent + '/'))
+
+ def test_uses_default_if_parent_not_specified(self):
+ t = morphlib.tempdir.Tempdir()
+ shutil.rmtree(t.dirname)
+ self.assertNotEqual(t.dirname, None)
+
+ def test_removes_itself(self):
+ dirname = self.tempdir.dirname
+ self.tempdir.remove()
+ self.assertEqual(self.tempdir.dirname, None)
+ self.assertFalse(os.path.exists(dirname))
+
+ def test_joins_filename(self):
+ self.assertEqual(self.tempdir.join('foo'),
+ os.path.join(self.tempdir.dirname, 'foo'))
+
+ def test_joins_absolute_filename(self):
+ self.assertEqual(self.tempdir.join('/foo'),
+ os.path.join(self.tempdir.dirname, 'foo'))
+
+ def test_clears_when_empty(self):
+ self.tempdir.clear()
+ self.assertEqual(os.listdir(self.tempdir.dirname), [])
+
+ def test_clears_when_not_empty(self):
+ os.mkdir(self.tempdir.join('foo'))
+ with open(self.tempdir.join('bar'), 'w') as f:
+ f.write('bar')
+ self.tempdir.clear()
+ self.assertEqual(os.listdir(self.tempdir.dirname), [])
+
diff --git a/morphlib/util.py b/morphlib/util.py
new file mode 100644
index 00000000..a181ff29
--- /dev/null
+++ b/morphlib/util.py
@@ -0,0 +1,32 @@
+# Copyright (C) 2011 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; either 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.
+
+
+'''Utility functions for morph.'''
+
+
+def indent(string, spaces=4):
+ '''Return ``string`` indented by ``spaces`` spaces.
+
+ The final line is not terminated by a newline. This makes it easy
+ to use this function for indenting long text for logging: the
+ logging library adds a newline, so not including it in the indented
+ text avoids a spurious empty line in the log file.
+
+ '''
+
+ return '\n'.join('%*s%s' % (spaces, '', line)
+ for line in string.splitlines())
+
diff --git a/morphlib/util_tests.py b/morphlib/util_tests.py
new file mode 100644
index 00000000..d27de771
--- /dev/null
+++ b/morphlib/util_tests.py
@@ -0,0 +1,36 @@
+# Copyright (C) 2011 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; either 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 unittest
+
+import morphlib
+
+
+class IndentTests(unittest.TestCase):
+
+ def test_returns_empty_string_for_empty_string(self):
+ self.assertEqual(morphlib.util.indent(''), '')
+
+ def test_indents_single_line(self):
+ self.assertEqual(morphlib.util.indent('foo'), ' foo')
+
+ def test_obeys_spaces_setting(self):
+ self.assertEqual(morphlib.util.indent('foo', spaces=2), ' foo')
+
+ def test_indents_multiple_lines(self):
+ self.assertEqual(morphlib.util.indent('foo\nbar\n'),
+ ' foo\n bar')
+