diff --git a/README.md b/README.md index 1e71a52b..08226c6d 100644 --- a/README.md +++ b/README.md @@ -325,9 +325,17 @@ scripts in each phase. ### Dependencies ### -Each element has a file named element-deps: a plain text, newline separated -list of elements which will be added to the list of elements built into the -image at image creation time. +Each element can use the following files to define or affect dependencies: + +* element-deps: a plain text, newline separated list of elements which will + be added to the list of elements built into the image at image creation time. + +* element-provides: A plain text, newline separated list of elements which + are provided by this element. These elements will be excluded from elements + built into the image at image creation time. For example if element A depends + on element B and element C includes element B in its "element-provides" + file and A and C are included when building an image, then B is not used. + ### First-boot files ### diff --git a/diskimage_builder/element_dependencies.py b/diskimage_builder/element_dependencies.py index 372cf964..76c63a86 100644 --- a/diskimage_builder/element_dependencies.py +++ b/diskimage_builder/element_dependencies.py @@ -24,21 +24,12 @@ def get_elements_dir(): return os.environ['ELEMENTS_PATH'] -def dependencies(element, elements_dir=None): - """Return the non-transitive list of dependencies for a single element. - - :param user_elements: iterable enumerating elements a user has requested - :param elements_dir: the elements dir to read from. If not supplied, - inferred by calling get_elements_dir(). - - :return: a set just containing all elements that the specified element - depends on. - """ +def _get_set(element, fname, elements_dir=None): if elements_dir is None: elements_dir = get_elements_dir() for path in elements_dir.split(':'): - element_deps_path = (os.path.join(path, element, 'element-deps')) + element_deps_path = (os.path.join(path, element, fname)) try: with open(element_deps_path) as element_deps: return set([line.strip() for line in element_deps]) @@ -55,6 +46,32 @@ def dependencies(element, elements_dir=None): exit(-1) +def provides(element, elements_dir=None): + """Return the set of elements provided by the specified element. + + :param element: name of a single element + :param elements_dir: the elements dir to read from. If not supplied, + inferred by calling get_elements_dir(). + + :return: a set just containing all elements that the specified element + provides. + """ + return _get_set(element, 'element-provides', elements_dir) + + +def dependencies(element, elements_dir=None): + """Return the non-transitive set of dependencies for a single element. + + :param element: name of a single element + :param elements_dir: the elements dir to read from. If not supplied, + inferred by calling get_elements_dir(). + + :return: a set just containing all elements that the specified element + depends on. + """ + return _get_set(element, 'element-deps', elements_dir) + + def expand_dependencies(user_elements, elements_dir=None): """Expand user requested elements using element-deps files. @@ -68,14 +85,24 @@ def expand_dependencies(user_elements, elements_dir=None): """ final_elements = set(user_elements) check_queue = list(user_elements) + provided = set() while check_queue: element = check_queue.pop() + if element in provided: + continue deps = dependencies(element, elements_dir) - check_queue.extend(deps - final_elements) + provided.update(provides(element, elements_dir)) + check_queue.extend(deps - (final_elements | provided)) final_elements.update(deps) - return final_elements + conflicts = set(user_elements) & provided + if conflicts: + sys.stderr.write("ERROR: Following elements were explicitly required " + "but are provided by other included elements: %s\n" % + ", ".join(conflicts)) + exit(-1) + return final_elements - provided def main(argv): diff --git a/diskimage_builder/tests/test_elementdeps.py b/diskimage_builder/tests/test_elementdeps.py index fc99f0ba..0c945d29 100644 --- a/diskimage_builder/tests/test_elementdeps.py +++ b/diskimage_builder/tests/test_elementdeps.py @@ -24,12 +24,15 @@ data_dir = os.path.abspath( os.path.join(os.path.dirname(__file__), 'test-elements')) -def _populate_element(element_dir, element_name, element_deps=[]): +def _populate_element(element_dir, element_name, element_deps=[], provides=[]): element_home = os.path.join(element_dir, element_name) os.mkdir(element_home) deps_path = os.path.join(element_home, 'element-deps') with open(deps_path, 'w') as deps_file: deps_file.write("\n".join(element_deps)) + provides_path = os.path.join(element_home, 'element-provides') + with open(provides_path, 'w') as provides_file: + provides_file.write("\n".join(provides)) class TestElementDeps(testtools.TestCase): @@ -43,6 +46,12 @@ class TestElementDeps(testtools.TestCase): 'requires-requires-foo', ['requires-foo']) _populate_element(self.element_dir, 'self', ['self']) + _populate_element(self.element_dir, + 'provides_virtual', + [], + ['virtual']) + _populate_element(self.element_dir, 'requires_virtual', ['virtual']) + _populate_element(self.element_dir, 'virtual', []) _populate_element(self.element_dir, 'circular1', ['circular2']) _populate_element(self.element_dir, 'circular2', ['circular1']) @@ -79,6 +88,18 @@ class TestElementDeps(testtools.TestCase): ['circular1'], elements_dir=self.element_dir) self.assertEqual(set(['circular1', 'circular2']), result) + def test_provide(self): + result = element_dependencies.expand_dependencies( + ['requires_virtual', 'provides_virtual'], + elements_dir=self.element_dir) + self.assertEqual(set(['requires_virtual', 'provides_virtual']), result) + + def test_provide_conflict(self): + self.assertRaises(SystemExit, + element_dependencies.expand_dependencies, + ['virtual', 'provides_virtual'], + self.element_dir) + class TestElements(testtools.TestCase): def test_depends_on_env(self):