summaryrefslogtreecommitdiff
path: root/morphlib
diff options
context:
space:
mode:
authorSam Thursfield <sam.thursfield@codethink.co.uk>2015-07-28 15:23:09 +0000
committerSam Thursfield <sam.thursfield@codethink.co.uk>2015-09-17 10:39:25 +0000
commitb652fc3cb55a238293c246f2bfad2a18ade39aef (patch)
treeaf68bafdb62574e22e38e2e68cbbf55a514d34f7 /morphlib
parentb548e0e5094b1bfdc6f9242c4f26ce191c7b8aad (diff)
downloadmorph-b652fc3cb55a238293c246f2bfad2a18ade39aef.tar.gz
Add support for Baserock definitions version 7
This adds a new 'Defaults' class to represent definitions defaults The Python 'jsonschema' module is used to validate the contents of the Defaults file. This module is already included in Baserock 'build' and 'devel' reference systems by way of the 'openstack-common' stratum. This commit embeds a copy of the JSON-Schema schema for the DEFAULTS file. I think the canonical location of this schema should be in the reference definitions.git, for now. In future, the schemas should maybe have their own repos. Either way, Morph should embed a copy for the time being so that we are sure the schema matches how Morph expects to parse the file. Morph's automated tests are all updated to use definitions version 7. I removed most of the tests for built-in build systems, because the built-ins themselves are no longer part of Morph. Only the mechanism for defining them needs to be tested now. Change-Id: I65f8f1c967683ef605852bfae5c68518e53f9981
Diffstat (limited to 'morphlib')
-rw-r--r--morphlib/__init__.py1
-rw-r--r--morphlib/artifactresolver_tests.py88
-rw-r--r--morphlib/artifactsplitrule.py18
-rw-r--r--morphlib/buildsystem.py19
-rw-r--r--morphlib/buildsystem_tests.py9
-rw-r--r--morphlib/cachekeycomputer_tests.py12
-rw-r--r--morphlib/defaults.py108
-rw-r--r--morphlib/definitions_repo.py19
-rw-r--r--morphlib/definitions_version.py2
-rw-r--r--morphlib/morphloader.py38
-rw-r--r--morphlib/morphloader_tests.py43
-rw-r--r--morphlib/morphologyfinder.py14
-rw-r--r--morphlib/morphologyfinder_tests.py4
-rw-r--r--morphlib/schemas/defaults.json-schema66
-rw-r--r--morphlib/source.py9
-rw-r--r--morphlib/sourceresolver.py84
-rw-r--r--morphlib/sourceresolver_tests.py8
-rw-r--r--morphlib/util.py6
18 files changed, 425 insertions, 123 deletions
diff --git a/morphlib/__init__.py b/morphlib/__init__.py
index 81540a31..36e5c3a8 100644
--- a/morphlib/__init__.py
+++ b/morphlib/__init__.py
@@ -60,6 +60,7 @@ import builder
import cachedrepo
import cachekeycomputer
import cmdline_parse_utils
+import defaults
import definitions_repo
import definitions_version
import extensions
diff --git a/morphlib/artifactresolver_tests.py b/morphlib/artifactresolver_tests.py
index 141ff948..52ed0811 100644
--- a/morphlib/artifactresolver_tests.py
+++ b/morphlib/artifactresolver_tests.py
@@ -20,6 +20,12 @@ import yaml
import morphlib
+default_split_rules = {
+ 'chunk': morphlib.artifactsplitrule.DEFAULT_CHUNK_RULES,
+ 'stratum': morphlib.artifactsplitrule.DEFAULT_STRATUM_RULES,
+}
+
+
def get_chunk_morphology(name, artifact_names=[]):
assert(isinstance(artifact_names, list))
@@ -88,9 +94,9 @@ class ArtifactResolverTests(unittest.TestCase):
pool = morphlib.sourcepool.SourcePool()
morph = get_chunk_morphology('chunk')
- sources = morphlib.source.make_sources('repo', 'ref',
- 'chunk.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'ref', 'chunk.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for source in sources:
pool.add(source)
@@ -109,9 +115,9 @@ class ArtifactResolverTests(unittest.TestCase):
pool = morphlib.sourcepool.SourcePool()
morph = get_chunk_morphology('chunk', ['chunk-foobar'])
- sources = morphlib.source.make_sources('repo', 'ref',
- 'chunk.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'ref', 'chunk.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for source in sources:
pool.add(source)
@@ -129,9 +135,9 @@ class ArtifactResolverTests(unittest.TestCase):
pool = morphlib.sourcepool.SourcePool()
morph = get_chunk_morphology('chunk', ['chunk-baz', 'chunk-qux'])
- sources = morphlib.source.make_sources('repo', 'ref',
- 'chunk.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'ref', 'chunk.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for source in sources:
pool.add(source)
@@ -151,18 +157,18 @@ class ArtifactResolverTests(unittest.TestCase):
pool = morphlib.sourcepool.SourcePool()
morph = get_chunk_morphology('chunk')
- sources = morphlib.source.make_sources('repo', 'ref',
- 'chunk.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'ref', 'chunk.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for chunk in sources:
pool.add(chunk)
morph = get_stratum_morphology(
'stratum', chunks=[('chunk', 'chunk', 'repo', 'ref')])
- stratum_sources = set(morphlib.source.make_sources('repo', 'ref',
- 'stratum.morph',
- 'sha1', 'tree',
- morph))
+ stratum_sources = set(
+ morphlib.source.make_sources(
+ 'repo', 'ref', 'stratum.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules))
for stratum in stratum_sources:
pool.add(stratum)
@@ -197,9 +203,9 @@ class ArtifactResolverTests(unittest.TestCase):
pool = morphlib.sourcepool.SourcePool()
morph = get_chunk_morphology('chunk', ['chunk-foo', 'chunk-bar'])
- sources = morphlib.source.make_sources('repo', 'ref',
- 'chunk.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'ref', 'chunk.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for chunk in sources:
pool.add(chunk)
@@ -208,10 +214,10 @@ class ArtifactResolverTests(unittest.TestCase):
chunks=[
('chunk', 'chunk', 'repo', 'ref'),
])
- stratum_sources = set(morphlib.source.make_sources('repo', 'ref',
- 'stratum.morph',
- 'sha1', 'tree',
- morph))
+ stratum_sources = set(
+ morphlib.source.make_sources(
+ 'repo', 'ref', 'stratum.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules))
for stratum in stratum_sources:
pool.add(stratum)
@@ -246,7 +252,8 @@ class ArtifactResolverTests(unittest.TestCase):
chunk = get_chunk_morphology('chunk1')
chunk1, = morphlib.source.make_sources(
- 'repo', 'original/ref', 'chunk1.morph', 'sha1', 'tree', chunk)
+ 'repo', 'original/ref', 'chunk1.morph', 'sha1', 'tree', chunk,
+ default_split_rules=default_split_rules)
pool.add(chunk1)
morph = get_stratum_morphology(
@@ -254,15 +261,16 @@ class ArtifactResolverTests(unittest.TestCase):
chunks=[(loader.save_to_string(chunk), 'chunk1.morph',
'repo', 'original/ref')],
build_depends=['stratum2'])
- sources = morphlib.source.make_sources('repo', 'original/ref',
- 'stratum1.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', 'stratum1.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for stratum1 in sources:
pool.add(stratum1)
chunk = get_chunk_morphology('chunk2')
chunk2, = morphlib.source.make_sources(
- 'repo', 'original/ref', 'chunk2.morph', 'sha1', 'tree', chunk)
+ 'repo', 'original/ref', 'chunk2.morph', 'sha1', 'tree', chunk,
+ default_split_rules=default_split_rules)
pool.add(chunk2)
morph = get_stratum_morphology(
@@ -270,9 +278,9 @@ class ArtifactResolverTests(unittest.TestCase):
chunks=[(loader.save_to_string(chunk), 'chunk2.morph',
'repo', 'original/ref')],
build_depends=['stratum1'])
- sources = morphlib.source.make_sources('repo', 'original/ref',
- 'stratum2.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', 'stratum2.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for stratum2 in sources:
pool.add(stratum2)
@@ -299,23 +307,23 @@ class ArtifactResolverTests(unittest.TestCase):
ref: original/ref
build-depends: []
''')
- sources = morphlib.source.make_sources('repo', 'original/ref',
- 'stratum.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', 'stratum.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for stratum in sources:
pool.add(stratum)
morph = get_chunk_morphology('chunk1')
- sources = morphlib.source.make_sources('repo', 'original/ref',
- 'chunk1.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', 'chunk1.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for chunk1 in sources:
pool.add(chunk1)
morph = get_chunk_morphology('chunk2')
- sources = morphlib.source.make_sources('repo', 'original/ref',
- 'chunk2.morph', 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', 'chunk2.morph', 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for chunk2 in sources:
pool.add(chunk2)
diff --git a/morphlib/artifactsplitrule.py b/morphlib/artifactsplitrule.py
index b5ebdf83..ba5abe02 100644
--- a/morphlib/artifactsplitrule.py
+++ b/morphlib/artifactsplitrule.py
@@ -189,10 +189,6 @@ class SplitRules(collections.Iterable):
for artifact, rule in self._rules)
-# TODO: Work out a good way to feed new defaults in. This is good for
-# the usual Linux userspace, but we may find issues and need a
-# migration path to a more useful set, or develop a system with
-# a different layout, like Android.
DEFAULT_CHUNK_RULES = [
('-bins', [ r"(usr/)?s?bin/.*" ]),
('-libs', [
@@ -229,6 +225,14 @@ DEFAULT_STRATUM_RULES = [
]
+# A 'no-op' set of split rules. An empty list would cause everything to be
+# ignored, which is unlikely to ever be what a user wants, and breaks some
+# internal bits of Morph.
+EMPTY_RULES = [
+ ('', [r'.*'])
+]
+
+
def unify_chunk_matches(morphology, default_rules=DEFAULT_CHUNK_RULES):
'''Create split rules including defaults and per-chunk rules.
@@ -237,6 +241,8 @@ def unify_chunk_matches(morphology, default_rules=DEFAULT_CHUNK_RULES):
by building the chunk to the chunk artifact they should be put in.
'''
+ if default_rules is None or len(default_rules) == 0:
+ default_rules = EMPTY_RULES
split_rules = SplitRules()
@@ -265,6 +271,8 @@ def unify_stratum_matches(morphology, default_rules=DEFAULT_STRATUM_RULES):
strata to the stratum artifact they should be put in.
'''
+ if default_rules is None or len(default_rules) == 0:
+ default_rules = EMPTY_RULES
assignment_split_rules = SplitRules()
for spec in morphology['chunks']:
@@ -296,7 +304,7 @@ def unify_stratum_matches(morphology, default_rules=DEFAULT_STRATUM_RULES):
match_split_rules))
-def unify_system_matches(morphology):
+def unify_system_matches(morphology, default_rules=[]):
'''Create split rules including defaults and per-chunk rules.
With rules specified in the morphology's 'products' field and the
diff --git a/morphlib/buildsystem.py b/morphlib/buildsystem.py
index 4655f2ee..95bf4116 100644
--- a/morphlib/buildsystem.py
+++ b/morphlib/buildsystem.py
@@ -44,15 +44,10 @@ _STRIP_COMMAND = r'''find "$DESTDIR" -type f \
class BuildSystem(object):
- '''An abstraction of an upstream build system.
+ '''Predefined command sequences for a given build system.
- Some build systems are well known: autotools, for example.
- Others are purely manual: there's a set of commands to run that
- are specific for that project, and (almost) no other project uses them.
- The Linux kernel would be an example of that.
-
- This class provides an abstraction for these, including a method
- to autodetect well known build systems.
+ For example, you can have an 'autotools' build system, which runs
+ 'configure', 'make' and 'make install'.
'''
@@ -73,6 +68,14 @@ class BuildSystem(object):
self.strip_commands = []
self.post_strip_commands = []
+ def from_dict(self, name, commands):
+ self.name = name
+
+ self.configure_commands = commands.get('configure-commands', [])
+ self.build_commands = commands.get('build-commands', [])
+ self.install_commands = commands.get('install-commands', [])
+ self.strip_commands = commands.get('strip-commands', [])
+
def __getitem__(self, key):
key = '_'.join(key.split('-'))
return getattr(self, key)
diff --git a/morphlib/buildsystem_tests.py b/morphlib/buildsystem_tests.py
index 80898ebd..506698a5 100644
--- a/morphlib/buildsystem_tests.py
+++ b/morphlib/buildsystem_tests.py
@@ -53,6 +53,15 @@ class BuildSystemTests(unittest.TestCase):
morph = self.bs.get_morphology('foobar')
self.assertTrue(morph.__class__.__name__ == 'Morphology')
+ def test_construct_from_dict(self):
+ '''Test parsing a dict of information from a DEFAULTS file.'''
+
+ commands_dict = {
+ 'configure-commands': 'foo'
+ }
+ self.bs.from_dict('test', commands_dict)
+ self.assertEqual(self.bs.configure_commands, 'foo')
+
class ManualBuildSystemTests(unittest.TestCase):
diff --git a/morphlib/cachekeycomputer_tests.py b/morphlib/cachekeycomputer_tests.py
index aa217dfc..2a5852fe 100644
--- a/morphlib/cachekeycomputer_tests.py
+++ b/morphlib/cachekeycomputer_tests.py
@@ -28,6 +28,12 @@ class DummyBuildEnvironment:
self.env = env
+default_split_rules = {
+ 'chunk': morphlib.artifactsplitrule.DEFAULT_CHUNK_RULES,
+ 'stratum': morphlib.artifactsplitrule.DEFAULT_STRATUM_RULES,
+}
+
+
class CacheKeyComputerTests(unittest.TestCase):
def setUp(self):
@@ -83,9 +89,9 @@ class CacheKeyComputerTests(unittest.TestCase):
''',
}.iteritems():
morph = loader.load_from_string(text)
- sources = morphlib.source.make_sources('repo', 'original/ref',
- name, 'sha1',
- 'tree', morph)
+ sources = morphlib.source.make_sources(
+ 'repo', 'original/ref', name, 'sha1', 'tree', morph,
+ default_split_rules=default_split_rules)
for source in sources:
self.source_pool.add(source)
self.build_env = DummyBuildEnvironment({
diff --git a/morphlib/defaults.py b/morphlib/defaults.py
new file mode 100644
index 00000000..9e695a90
--- /dev/null
+++ b/morphlib/defaults.py
@@ -0,0 +1,108 @@
+# Copyright (C) 2015 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, see <http://www.gnu.org/licenses/>.
+#
+# =*= License: GPL-2 =*=
+
+
+import cliapp
+import jsonschema
+import yaml
+
+import os
+
+import morphlib
+
+
+class Defaults(object):
+ '''Represents predefined default values specific to Baserock definitions.
+
+ The DEFAULTS file was added in definitions format version 7, which lets
+ users set these defaults. The text of DEFAULTS file can be passed in as
+ 'text', and will be validated and parsed if definitions_version >= 7.
+
+ Prior to version 7, the defaults were hardcoded in Morph. These defaults
+ will be returned if definitions_version < 7.
+
+ '''
+ def __init__(self, definitions_version, text=None):
+ self._build_systems = {}
+ self._split_rules = {}
+
+ schema_path = os.path.join(morphlib.util.schemas_directory(),
+ 'defaults.json-schema')
+ with open(schema_path) as f:
+ self.schema = yaml.load(f)
+
+ if definitions_version >= 7:
+ if text:
+ self._build_systems, self._split_rules = self._parse(text)
+ else:
+ self._build_systems, self._split_rules = self._builtins()
+
+ def _parse(self, text):
+ build_systems = {}
+ split_rules = {}
+
+ # This reports errors against <string> rather than the actual filename,
+ # which is sad.
+ data = yaml.safe_load(text)
+
+ if data is None:
+ # It's OK to be empty, I guess.
+ return build_systems, split_rules
+
+ try:
+ # It would be nice if this could give line numbers when it spotted
+ # errors. Seems tricky.
+ jsonschema.validate(data, self.schema)
+ except jsonschema.ValidationError as e:
+ raise cliapp.AppException('Invalid DEFAULTS file: %s' % e.message)
+
+ build_system_data = data.get('build-systems', {})
+ for name, commands in build_system_data.items():
+ build_system = morphlib.buildsystem.BuildSystem()
+ build_system.from_dict(name, commands)
+ build_systems[name] = build_system
+
+ # It would make sense to create artifactsplitrule.SplitRule instances
+ # here, instead of an unlabelled data structure. That would need some
+ # changes to source.make_sources() and the 'artifactsplitrule' module.
+ split_rules_data = data.get('split-rules', {})
+ for kind, rules in split_rules_data.items():
+ split_rules[kind] = []
+ for rule in rules:
+ rule_unlabelled = (rule['artifact'], rule['include'])
+ split_rules[kind].append(rule_unlabelled)
+
+ return build_systems, split_rules
+
+ def _builtins(self):
+ build_systems = {}
+ split_rules = {}
+
+ for build_system in morphlib.buildsystem.build_systems:
+ build_systems[build_system.name] = build_system
+
+ split_rules['chunk'] = \
+ morphlib.artifactsplitrule.DEFAULT_CHUNK_RULES
+ split_rules['stratum'] = \
+ morphlib.artifactsplitrule.DEFAULT_STRATUM_RULES
+
+ return build_systems, split_rules
+
+ def build_systems(self):
+ return self._build_systems
+
+ def split_rules(self):
+ return self._split_rules
diff --git a/morphlib/definitions_repo.py b/morphlib/definitions_repo.py
index 53328e85..143b5241 100644
--- a/morphlib/definitions_repo.py
+++ b/morphlib/definitions_repo.py
@@ -23,6 +23,7 @@ import logging
import os
import urlparse
import uuid
+import warnings
import morphlib
import gitdir
@@ -238,8 +239,24 @@ class DefinitionsRepo(gitdir.GitDirectory):
version_text = mf.read_file('VERSION')
version = morphlib.definitions_version.check_version_file(version_text)
+ defaults_text = mf.read_file('DEFAULTS', allow_missing=True)
+
+ if version < 7:
+ if defaults_text is not None:
+ warnings.warn(
+ "Ignoring DEFAULTS file, because these definitions are "
+ "version %i" % version)
+ defaults_text = None
+ else:
+ if defaults_text is None:
+ warnings.warn("No DEFAULTS file found.")
+
+ defaults = morphlib.defaults.Defaults(version,
+ text=defaults_text)
+
loader = morphlib.morphloader.MorphologyLoader(
- definitions_version=version)
+ definitions_version=version,
+ predefined_build_systems=defaults.build_systems())
return loader
diff --git a/morphlib/definitions_version.py b/morphlib/definitions_version.py
index 56cef164..ec2df928 100644
--- a/morphlib/definitions_version.py
+++ b/morphlib/definitions_version.py
@@ -24,7 +24,7 @@ import yaml
import morphlib
-SUPPORTED_VERSIONS = [3, 4, 5, 6]
+SUPPORTED_VERSIONS = [3, 4, 5, 6, 7]
class DefinitionsVersionError(cliapp.AppException):
diff --git a/morphlib/morphloader.py b/morphlib/morphloader.py
index 479bc8fb..e58b3164 100644
--- a/morphlib/morphloader.py
+++ b/morphlib/morphloader.py
@@ -111,6 +111,13 @@ class UnknownArchitectureError(MorphologyValidationError):
% (arch, morph_filename))
+class UnknownBuildSystemError(MorphologyValidationError):
+
+ def __init__(self, build_system, morph_filename):
+ self.msg = ('Undefined build system %s in morphology %s'
+ % (build_system, morph_filename))
+
+
class NoStratumBuildDependenciesError(MorphologyValidationError):
def __init__(self, stratum_name, morph_filename):
@@ -403,7 +410,7 @@ class MorphologyLoader(object):
}
def __init__(self, definitions_version=0,
- lookup_build_system=morphlib.buildsystem.lookup_build_system):
+ predefined_build_systems={}):
if definitions_version >= 5: # pragma: no cover
self._static_defaults = copy.deepcopy(self._static_defaults)
self._static_defaults['chunk'].update({
@@ -412,7 +419,12 @@ class MorphologyLoader(object):
'post-strip-commands': None})
self._definitions_version = definitions_version
- self._lookup_build_system = lookup_build_system
+
+ self._predefined_build_systems = predefined_build_systems.copy()
+
+ if 'manual' not in self._predefined_build_systems:
+ self._predefined_build_systems['manual'] = \
+ morphlib.buildsystem.ManualBuildSystem()
def parse_morphology_text(self, text, morph_filename):
'''Parse a textual morphology.
@@ -820,10 +832,12 @@ class MorphologyLoader(object):
if morph['max-jobs'] is not None:
morph['max-jobs'] = int(morph['max-jobs'])
- def _unset_chunk_defaults(self, morph): # pragma: no cover
+ def _unset_chunk_defaults(self, morph): # pragma: no cover
+ # This is only used by the deprecated branch-and-merge plugin, and
+ # probably doesn't work correctly for definitions V7 and newer.
default_bs = self._static_defaults['chunk']['build-system']
- bs = self._lookup_build_system(
- morph.get('build-system', default_bs))
+ bs_name = morph.get('build-system', default_bs)
+ bs = self._predefined_build_systems[bs_name]
for key in self._static_defaults['chunk']:
if key not in morph: continue
if 'commands' not in key: continue
@@ -832,11 +846,19 @@ class MorphologyLoader(object):
if morph[key] == default_value:
del morph[key]
+ def lookup_build_system(self, name):
+ return self._predefined_build_systems[name]
+
def set_commands(self, morph):
- default = self._static_defaults['chunk']['build-system']
- bs = self._lookup_build_system(
- morph.get('build-system', default))
if morph['kind'] == 'chunk':
+ default = self._static_defaults['chunk']['build-system']
+ bs_name = morph.get('build-system', default)
+
+ try:
+ bs = self.lookup_build_system(bs_name)
+ except KeyError:
+ raise UnknownBuildSystemError(bs_name, morph['name'])
+
for key in self._static_defaults['chunk']:
if 'commands' not in key: continue
if key not in morph:
diff --git a/morphlib/morphloader_tests.py b/morphlib/morphloader_tests.py
index 6cb93094..1a2ee107 100644
--- a/morphlib/morphloader_tests.py
+++ b/morphlib/morphloader_tests.py
@@ -50,7 +50,7 @@ class MorphologyLoaderTests(unittest.TestCase):
def setUp(self):
self.loader = morphlib.morphloader.MorphologyLoader(
- definitions_version=6)
+ definitions_version=7)
self.tempdir = tempfile.mkdtemp()
self.filename = os.path.join(self.tempdir, 'foo.morph')
@@ -61,12 +61,12 @@ class MorphologyLoaderTests(unittest.TestCase):
string = '''\
name: foo
kind: chunk
-build-system: dummy
+build-system: manual
'''
morph = self.loader.parse_morphology_text(string, 'test')
self.assertEqual(morph['kind'], 'chunk')
self.assertEqual(morph['name'], 'foo')
- self.assertEqual(morph['build-system'], 'dummy')
+ self.assertEqual(morph['build-system'], 'manual')
def test_fails_to_parse_utter_garbage(self):
self.assertRaises(
@@ -495,43 +495,43 @@ chunks:
string = '''\
name: foo
kind: chunk
-build-system: dummy
+build-system: manual
'''
morph = self.loader.load_from_string(string)
self.assertEqual(morph['kind'], 'chunk')
self.assertEqual(morph['name'], 'foo')
- self.assertEqual(morph['build-system'], 'dummy')
+ self.assertEqual(morph['build-system'], 'manual')
def test_loads_json_from_string(self):
string = '''\
{
"name": "foo",
"kind": "chunk",
- "build-system": "dummy"
+ "build-system": "manual"
}
'''
morph = self.loader.load_from_string(string)
self.assertEqual(morph['kind'], 'chunk')
self.assertEqual(morph['name'], 'foo')
- self.assertEqual(morph['build-system'], 'dummy')
+ self.assertEqual(morph['build-system'], 'manual')
def test_loads_from_file(self):
with open(self.filename, 'w') as f:
f.write('''\
name: foo
kind: chunk
-build-system: dummy
+build-system: manual
''')
morph = self.loader.load_from_file(self.filename)
self.assertEqual(morph['kind'], 'chunk')
self.assertEqual(morph['name'], 'foo')
- self.assertEqual(morph['build-system'], 'dummy')
+ self.assertEqual(morph['build-system'], 'manual')
def test_saves_to_string(self):
morph = morphlib.morphology.Morphology({
'name': 'foo',
'kind': 'chunk',
- 'build-system': 'dummy',
+ 'build-system': 'manual',
})
text = self.loader.save_to_string(morph)
@@ -540,14 +540,14 @@ build-system: dummy
self.assertEqual(text, '''\
name: foo
kind: chunk
-build-system: dummy
+build-system: manual
''')
def test_saves_to_file(self):
morph = morphlib.morphology.Morphology({
'name': 'foo',
'kind': 'chunk',
- 'build-system': 'dummy',
+ 'build-system': 'manual',
})
self.loader.save_to_file(self.filename, morph)
@@ -559,7 +559,7 @@ build-system: dummy
self.assertEqual(text, '''\
name: foo
kind: chunk
-build-system: dummy
+build-system: manual
''')
def test_validate_does_not_set_defaults(self):
@@ -967,12 +967,11 @@ build-system: dummy
)
s = self.loader.save_to_string(m)
- def test_smoketest_strip_commands(self):
- dummy_buildsystem = morphlib.buildsystem.DummyBuildSystem()
- loader = morphlib.morphloader.MorphologyLoader(
- definitions_version=5,
- lookup_build_system=lambda x: dummy_buildsystem)
- m = morphlib.morphology.Morphology(
- {'name': 'test', 'kind': 'chunk', 'build-system': 'dummy'})
- loader.set_commands(m)
- self.assertEqual(m['strip-commands'], dummy_buildsystem.strip_commands)
+ def test_unknown_build_system(self):
+ m = morphlib.morphology.Morphology({
+ 'kind': 'chunk',
+ 'name': 'foo',
+ 'build-system': 'monkey scientist',
+ })
+ with self.assertRaises(morphlib.morphloader.UnknownBuildSystemError):
+ s = self.loader.set_commands(m)
diff --git a/morphlib/morphologyfinder.py b/morphlib/morphologyfinder.py
index 335c01b6..2c864bea 100644
--- a/morphlib/morphologyfinder.py
+++ b/morphlib/morphologyfinder.py
@@ -15,9 +15,7 @@
# =*= License: GPL-2 =*=
-import cliapp
-
-import morphlib
+import errno
class MorphologyFinder(object):
@@ -33,7 +31,7 @@ class MorphologyFinder(object):
self.gitdir = gitdir
self.ref = ref
- def read_file(self, filename):
+ def read_file(self, filename, allow_missing=False):
'''Return the un-parsed text of a morphology.
For the given morphology name, locate and return the contents
@@ -43,7 +41,13 @@ class MorphologyFinder(object):
is handled by the MorphologyLoader class.
'''
- return self.gitdir.read_file(filename, self.ref)
+ try:
+ return self.gitdir.read_file(filename, self.ref)
+ except IOError as e:
+ if allow_missing and e.errno == errno.ENOENT:
+ return None
+ else:
+ raise
def list_morphologies(self):
'''Return the filenames of all morphologies in the (repo, ref).
diff --git a/morphlib/morphologyfinder_tests.py b/morphlib/morphologyfinder_tests.py
index 59772e23..3312b627 100644
--- a/morphlib/morphologyfinder_tests.py
+++ b/morphlib/morphologyfinder_tests.py
@@ -103,6 +103,10 @@ class MorphologyFinderTests(unittest.TestCase):
mf = morphlib.morphologyfinder.MorphologyFinder(gd)
self.assertEqual(mf.read_file('foo.morph'),
"altered morphology text")
+ with self.assertRaises(IOError):
+ mf.read_file('missing')
+ self.assertEqual(mf.read_file('missing', allow_missing=True),
+ None)
def test_read_morph_raises_no_worktree_no_ref(self):
gd = morphlib.gitdir.GitDirectory(self.mirror)
diff --git a/morphlib/schemas/defaults.json-schema b/morphlib/schemas/defaults.json-schema
new file mode 100644
index 00000000..2f713425
--- /dev/null
+++ b/morphlib/schemas/defaults.json-schema
@@ -0,0 +1,66 @@
+$schema: http://json-schema.org/draft-04/schema#
+id: http://git.baserock.org/cgi-bin/cgit.cgi/baserock/baserock/definitions.git/tree/schemas/defaults.json-schema
+
+description: |
+ This is a JSON-Schema description of the DEFAULTS file specified in the
+ Baserock definitions format. DEFAULTS is a YAML file that contains global
+ defaults for a set of Baserock definitions.
+
+ This JSON-Schema file is valid for VERSION 7 of the Baserock definitions
+ YAML serialisation format.
+
+ The Baserock definitions YAML serialisation format is the recommended way of
+ representing Baserock definitions on disk. The actual data model is described
+ separately. See <https://wiki.baserock.org/definitions> for more information.
+
+ This schema is represented as YAML, so that it can be edited more easily.
+ You may need to convert to JSON if using a JSON-Schema tool that expects
+ its input to be an actual string containing data serialised as JSON.
+
+definitions:
+ command-sequence:
+ type: array
+ items: {type: string}
+
+ build-system:
+ type: object
+ additionalProperties: false
+ properties:
+ build-commands: {$ref: '#/definitions/command-sequence'}
+ configure-commands: {$ref: '#/definitions/command-sequence'}
+ install-commands: {$ref: '#/definitions/command-sequence'}
+ strip-commands: {$ref: '#/definitions/command-sequence'}
+
+ split-rules:
+ type: array
+ items:
+ type: object
+
+ required: [artifact, include]
+ additionalProperties: false
+
+ properties:
+ artifact: {type: string}
+ include:
+ type: array
+ items:
+ type: string
+ format: regex
+
+type: object
+additionalProperties: false
+
+properties:
+ # Predefined build systems.
+ build-systems:
+ type: object
+ patternProperties:
+ ^.*$: {$ref: '#/definitions/build-system'}
+
+ # Predefined artifact splitting rules.
+ split-rules:
+ type: object
+ additionalProperties: false
+ properties:
+ chunk: {$ref: '#/definitions/split-rules'}
+ stratum: {$ref: '#/definitions/split-rules'}
diff --git a/morphlib/source.py b/morphlib/source.py
index 2c96bcbd..135c14cc 100644
--- a/morphlib/source.py
+++ b/morphlib/source.py
@@ -77,12 +77,14 @@ class Source(object):
return artifact in self.dependencies
-def make_sources(reponame, ref, filename, absref, tree, morphology):
+def make_sources(reponame, ref, filename, absref, tree, morphology,
+ default_split_rules={}):
kind = morphology['kind']
if kind in ('system', 'chunk'):
unifier = getattr(morphlib.artifactsplitrule,
'unify_%s_matches' % kind)
- split_rules = unifier(morphology)
+ split_rules = unifier(morphology,
+ default_rules=default_split_rules.get(kind, {}))
# chunk and system sources are named after the morphology
source_name = morphology['name']
source = morphlib.source.Source(source_name, reponame, ref,
@@ -93,7 +95,8 @@ def make_sources(reponame, ref, filename, absref, tree, morphology):
yield source
elif kind == 'stratum': # pragma: no cover
unifier = morphlib.artifactsplitrule.unify_stratum_matches
- split_rules = unifier(morphology)
+ split_rules = unifier(morphology,
+ default_rules=default_split_rules.get(kind, {}))
for name in split_rules.artifacts:
source = morphlib.source.Source(
name, # stratum source name is artifact name
diff --git a/morphlib/sourceresolver.py b/morphlib/sourceresolver.py
index cbab0f7f..42398ce7 100644
--- a/morphlib/sourceresolver.py
+++ b/morphlib/sourceresolver.py
@@ -21,6 +21,7 @@ import os
import pylru
import shutil
import tempfile
+import warnings
import yaml
import cliapp
@@ -289,6 +290,36 @@ class SourceResolver(object):
return morphlib.definitions_version.check_version_file(version_text)
+ def _get_defaults(self, definitions_checkout_dir,
+ definitions_version=7): # pragma: no cover
+ '''Return the default build system commands, and default split rules.
+
+ This function returns a tuple with two dicts.
+
+ The defaults are read from a file named DEFAULTS in the definitions
+ directory, if the definitions follow format version 7 or later. If the
+ definitions follow version 6 or earlier, hardcoded defaults are used.
+
+ '''
+ # Read default build systems and split rules from DEFAULTS file.
+ defaults_text = self._get_file_contents_from_definitions(
+ definitions_checkout_dir, 'DEFAULTS')
+
+ if definitions_version < 7:
+ if defaults_text is not None:
+ warnings.warn(
+ "Ignoring DEFAULTS file, because these definitions are "
+ "version %i" % definitions_version)
+ defaults_text = None
+ else:
+ if defaults_text is None:
+ warnings.warn("No DEFAULTS file found.")
+
+ defaults = morphlib.defaults.Defaults(definitions_version,
+ text=defaults_text)
+
+ return defaults.build_systems(), defaults.split_rules()
+
def _get_morphology(self, resolved_morphologies, definitions_checkout_dir,
definitions_repo, definitions_absref, morph_loader,
reponame, sha1, filename): # pragma: no cover
@@ -311,17 +342,12 @@ class SourceResolver(object):
return morph
- def _process_definitions_with_children(self,
- resolved_morphologies,
- definitions_checkout_dir,
- definitions_repo,
- definitions_ref,
- definitions_absref,
- definitions_tree,
- definitions_version,
- morph_loader,
- system_filenames,
- visit): # pragma: no cover
+ def _process_definitions_with_children(
+ self, resolved_morphologies, definitions_checkout_dir,
+ definitions_repo, definitions_ref, definitions_absref,
+ definitions_tree, definitions_version, morph_loader,
+ system_filenames, visit,
+ predefined_split_rules): # pragma: no cover
definitions_queue = collections.deque(system_filenames)
chunk_queue = set()
@@ -341,7 +367,8 @@ class SourceResolver(object):
raise MorphologyNotFoundError(filename)
visit(definitions_repo, definitions_ref, filename,
- definitions_absref, definitions_tree, morphology)
+ definitions_absref, definitions_tree, morphology,
+ predefined_split_rules)
if morphology['kind'] == 'cluster':
raise cliapp.AppException(
@@ -462,7 +489,8 @@ class SourceResolver(object):
resolved_buildsystems, definitions_checkout_dir,
definitions_repo, definitions_absref,
definitions_version, morph_loader, chunk_repo, chunk_ref,
- filename, chunk_buildsystem, visit): # pragma: no cover
+ filename, chunk_buildsystem, visit,
+ predefined_split_rules): # pragma: no cover
absref = None
tree = None
chunk_key = None
@@ -483,7 +511,8 @@ class SourceResolver(object):
if morphology:
absref, tree = self._resolve_ref(resolved_trees, chunk_repo,
chunk_ref)
- visit(chunk_repo, chunk_ref, filename, absref, tree, morphology)
+ visit(chunk_repo, chunk_ref, filename, absref, tree, morphology,
+ predefined_split_rules)
return
absref, tree = self._resolve_ref(resolved_trees, chunk_repo, chunk_ref)
@@ -552,7 +581,8 @@ class SourceResolver(object):
morphology = generate_morph_and_cache_buildsystem(
buildsystem)
- visit(chunk_repo, chunk_ref, filename, absref, tree, morphology)
+ visit(chunk_repo, chunk_ref, filename, absref, tree, morphology,
+ predefined_split_rules)
def traverse_morphs(self, definitions_repo, definitions_ref,
system_filenames,
@@ -582,9 +612,15 @@ class SourceResolver(object):
definitions_absref, definitions_checkout_dir)
definitions_version = self._check_version_file(
- definitions_checkout_dir)
+ definitions_checkout_dir)
+
+ predefined_build_systems, predefined_split_rules = \
+ self._get_defaults(
+ definitions_checkout_dir, definitions_version)
+
morph_loader = morphlib.morphloader.MorphologyLoader(
- definitions_version=definitions_version)
+ definitions_version=definitions_version,
+ predefined_build_systems=predefined_build_systems)
# First, process the system and its stratum morphologies. These
# will all live in the same Git repository, and will point to
@@ -593,7 +629,7 @@ class SourceResolver(object):
resolved_morphologies, definitions_checkout_dir,
definitions_repo, definitions_ref, definitions_absref,
definitions_tree, definitions_version, morph_loader,
- system_filenames, visit)
+ system_filenames, visit, predefined_split_rules)
# Now process all the chunks involved in the build.
for repo, ref, filename, buildsystem in chunk_queue:
@@ -602,7 +638,8 @@ class SourceResolver(object):
definitions_checkout_dir,
definitions_repo, definitions_absref,
definitions_version, morph_loader, repo,
- ref, filename, buildsystem, visit)
+ ref, filename, buildsystem, visit,
+ predefined_split_rules)
def create_source_pool(lrc, rrc, repo, ref, filenames, cachedir,
@@ -624,10 +661,11 @@ def create_source_pool(lrc, rrc, repo, ref, filenames, cachedir,
'''
pool = morphlib.sourcepool.SourcePool()
- def add_to_pool(reponame, ref, filename, absref, tree, morphology):
- sources = morphlib.source.make_sources(reponame, ref,
- filename, absref,
- tree, morphology)
+ def add_to_pool(reponame, ref, filename, absref, tree, morphology,
+ predefined_split_rules):
+ sources = morphlib.source.make_sources(
+ reponame, ref, filename, absref, tree, morphology,
+ predefined_split_rules)
for source in sources:
pool.add(source)
diff --git a/morphlib/sourceresolver_tests.py b/morphlib/sourceresolver_tests.py
index 5985579c..58512172 100644
--- a/morphlib/sourceresolver_tests.py
+++ b/morphlib/sourceresolver_tests.py
@@ -32,7 +32,7 @@ class FakeRemoteRepoCache(object):
return '''{
"name": "%s",
"kind": "chunk",
- "build-system": "dummy"
+ "build-system": "manual"
}''' % filename[:-len('.morph')]
return 'text'
@@ -46,12 +46,12 @@ class FakeLocalRepo(object):
'chunk.morph': '''
name: chunk
kind: chunk
- build-system: dummy
+ build-system: manual
''',
'chunk-split.morph': '''
name: chunk-split
kind: chunk
- build-system: dummy
+ build-system: manual
products:
- artifact: chunk-split-runtime
include: []
@@ -119,7 +119,7 @@ class FakeLocalRepo(object):
return '''
name: %s
kind: chunk
- build-system: dummy''' % filename[:-len('.morph')]
+ build-system: manual''' % filename[:-len('.morph')]
return 'text'
def list_files(self, ref, recurse):
diff --git a/morphlib/util.py b/morphlib/util.py
index a92b7f37..27139c66 100644
--- a/morphlib/util.py
+++ b/morphlib/util.py
@@ -731,3 +731,9 @@ def temp_dir(*args, **kwargs): #pragma: no cover
else:
if cleanup_on_success:
shutil.rmtree(td, ignore_errors=True)
+
+
+def schemas_directory(): # pragma: no cover
+ '''Returns a path to the schemas/ subdirectory of the 'morphlib' module.'''
+ code_dir = os.path.dirname(morphlib.__file__)
+ return os.path.join(code_dir, 'schemas')