
This is a re-factor of element_dependencies to achieve two things -- centralising override policy and storing path names. Firstly we want to make the override policy for elements completely explicit. Currently, elements that wish to copy parts of other elements walk ELEMENTS_PATH themselves and look for elements in IMAGE_ELEMENT. How they handle duplicate elements can differ, leading to inconsistent behaviour. We introduce logic in element-info to find elements in each of the directories in ELEMENT_PATHS in *reverse* order -- that is to say, earlier entries in the paths will overwrite later ones. For example ELEMENT_PATHS=foo:bar:baz will mean that "foo/element" will override "baz/element", since "foo" is first. This should be sane to anyone familiar with $PATH. Documentation is clarified around this point and a test-case is added. The second thing is that we want to keep the complete path of the elements we have chosen. We want the aforementioned elements that walk the element list to use these canonical paths to pickup files; this way they don't need to make local decisions about element overrides, but can simply iterate a list and copy/merge files if they exist. A follow-on change (I7092e1845942f249175933d67ab121188f3511fd) will expose this data in a separate variable that can be parsed by elements (a further follow-on I0a64b45e9f2cfa28e84b2859d76b065a6c4590f0 modifies the elements to use this information). Thus this does not change the status-quo -- elements that are walking ELEMENTS_PATH themselves and can/will continue doing that. Change-Id: I2a29861c67de2d25c595cb35d850e92807d26ac6
202 lines
6.7 KiB
Python
202 lines
6.7 KiB
Python
# Copyright 2013 Hewlett-Packard Development Company, L.P.
|
|
#
|
|
# Licensed under the Apache License, Version 2.0 (the "License"); you may
|
|
# not use this file except in compliance with the License. You may obtain
|
|
# a copy of the License at
|
|
#
|
|
# http://www.apache.org/licenses/LICENSE-2.0
|
|
#
|
|
# Unless required by applicable law or agreed to in writing, software
|
|
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
|
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
|
# License for the specific language governing permissions and limitations
|
|
# under the License.
|
|
|
|
from __future__ import print_function
|
|
import argparse
|
|
import collections
|
|
import errno
|
|
import logging
|
|
import os
|
|
import sys
|
|
|
|
import diskimage_builder.logging_config
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Element(object):
|
|
"""An element"""
|
|
def __init__(self, name, path):
|
|
"""A new element
|
|
|
|
:param name: The element name
|
|
:param path: Full path to element. element-deps and
|
|
element-provides files will be parsed
|
|
"""
|
|
self.name = name
|
|
self.path = path
|
|
self.provides = set()
|
|
self.depends = set()
|
|
|
|
# read the provides & depends files for this element into a
|
|
# set; if the element has them.
|
|
provides = os.path.join(path, 'element-provides')
|
|
depends = os.path.join(path, 'element-deps')
|
|
try:
|
|
with open(provides) as p:
|
|
self.provides = set([line.strip() for line in p])
|
|
except IOError as e:
|
|
if e.errno == errno.ENOENT:
|
|
pass
|
|
else:
|
|
raise
|
|
try:
|
|
with open(depends) as d:
|
|
self.depends = set([line.strip() for line in d])
|
|
except IOError as e:
|
|
if e.errno == errno.ENOENT:
|
|
pass
|
|
else:
|
|
raise
|
|
|
|
logger.debug("New element : %s", str(self))
|
|
|
|
def __str__(self):
|
|
return '%s p:<%s> d:<%s>' % (self.name,
|
|
','.join(self.provides),
|
|
','.join(self.depends))
|
|
|
|
|
|
def get_elements_dir():
|
|
if not os.environ.get('ELEMENTS_PATH'):
|
|
raise Exception("$ELEMENTS_PATH must be set.")
|
|
return os.environ['ELEMENTS_PATH']
|
|
|
|
|
|
def expand_dependencies(user_elements, all_elements):
|
|
"""Expand user requested elements using element-deps files.
|
|
|
|
Arguments:
|
|
:param user_elements: iterable enumerating the elements a user requested
|
|
:param all_elements: Element object dictionary from find_all_elements
|
|
|
|
:return: a set containing the names of user_elements and all
|
|
dependent elements including any transitive dependencies.
|
|
"""
|
|
final_elements = set(user_elements)
|
|
check_queue = collections.deque(user_elements)
|
|
provided = set()
|
|
provided_by = collections.defaultdict(list)
|
|
|
|
while check_queue:
|
|
# bug #1303911 - run through the provided elements first to avoid
|
|
# adding unwanted dependencies and looking for virtual elements
|
|
element = check_queue.popleft()
|
|
if element in provided:
|
|
continue
|
|
elif element not in all_elements:
|
|
logger.error("Element '%s' not found", element)
|
|
sys.exit(1)
|
|
|
|
element_obj = all_elements[element]
|
|
|
|
element_deps = element_obj.depends
|
|
element_provides = element_obj.provides
|
|
# save which elements provide another element for potential
|
|
# error message
|
|
for provide in element_provides:
|
|
provided_by[provide].append(element)
|
|
provided.update(element_provides)
|
|
check_queue.extend(element_deps - (final_elements | provided))
|
|
final_elements.update(element_deps)
|
|
|
|
if "operating-system" not in provided:
|
|
logger.error(
|
|
"Please include an operating system element.")
|
|
sys.exit(-1)
|
|
|
|
conflicts = set(user_elements) & provided
|
|
if conflicts:
|
|
logger.error(
|
|
"The following elements are already provided by another element")
|
|
for element in conflicts:
|
|
logger.error("%s : already provided by %s" %
|
|
(element, provided_by[element]))
|
|
sys.exit(-1)
|
|
|
|
return final_elements - provided
|
|
|
|
|
|
def find_all_elements(paths=None):
|
|
"""Build a dictionary Element() objects
|
|
|
|
Walk ELEMENTS_PATH and find all elements. Make an Element object
|
|
for each element we wish to consider. Note we process overrides
|
|
such that elements specified earlier in the ELEMENTS_PATH override
|
|
those seen later.
|
|
|
|
:param paths: A list of paths to find elements in. If None will
|
|
use ELEMENTS_PATH
|
|
|
|
:return: a dictionary of all elements
|
|
|
|
"""
|
|
|
|
all_elements = {}
|
|
|
|
# note we process the later entries *first*, so that earlier
|
|
# entries will override later ones. i.e. with
|
|
# ELEMENTS_PATH=path1:path2:path3
|
|
# we want the elements in "path1" to override "path3"
|
|
if not paths:
|
|
paths = reversed(get_elements_dir().split(':'))
|
|
for path in paths:
|
|
if not os.path.isdir(path):
|
|
logger.error("ELEMENT_PATH entry '%s' is not a directory", path)
|
|
sys.exit(1)
|
|
|
|
# In words : make a list of directories in "path". Since an
|
|
# element is a directory, this is our list of elements.
|
|
elements = [os.path.realpath(os.path.join(path, f))
|
|
for f in os.listdir(path)
|
|
if os.path.isdir(os.path.join(path, f))]
|
|
|
|
for element in elements:
|
|
# the element name is the last part of the full path in
|
|
# element (these are all directories, we know that from
|
|
# above)
|
|
name = os.path.basename(element)
|
|
|
|
new_element = Element(name, element)
|
|
if name in all_elements:
|
|
logger.warning("Element <%s> overrides <%s>",
|
|
new_element.path, all_elements[name].path)
|
|
|
|
all_elements[name] = new_element
|
|
|
|
return all_elements
|
|
|
|
|
|
def main(argv):
|
|
diskimage_builder.logging_config.setup()
|
|
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument('elements', nargs='+',
|
|
help='display dependencies of the given elements')
|
|
parser.add_argument('--expand-dependencies', '-d', action='store_true',
|
|
default=False,
|
|
help=('(DEPRECATED) print expanded dependencies '
|
|
'of all args'))
|
|
|
|
args = parser.parse_args(argv[1:])
|
|
|
|
all_elements = find_all_elements()
|
|
|
|
if args.expand_dependencies:
|
|
logger.warning("expand-dependencies flag is deprecated, "
|
|
"and is now on by default.", file=sys.stderr)
|
|
|
|
print(' '.join(expand_dependencies(args.elements, all_elements)))
|
|
return 0
|