diff options
author | Steve Fung <stevefung@google.com> | 2016-04-19 17:04:06 -0700 |
---|---|---|
committer | Treehugger Robot <treehugger-gerrit@google.com> | 2016-04-20 21:30:16 +0000 |
commit | bb621f2078fb37008f80c61c460daa6f42915812 (patch) | |
tree | 8a7939f1d038f89829524eb3a1842b28c7954986 | |
parent | 6d8c0d1759e35c654eef64c5e88955e1ab925073 (diff) | |
download | bdk-bb621f2078fb37008f80c61c460daa6f42915812.tar.gz |
Convert project/ to 4 space indent
Convert all files in the project/ folder to a 4 space indent
to comply with PEP8 style rules.
Bug: 28007659
Test: `python test_runner.py` passes.
Test: pylint passes.
Change-Id: Ie558e256e0c343d4786f5fd7f18a671446e79c41
-rw-r--r-- | cli/lib/project/acl.py | 383 | ||||
-rw-r--r-- | cli/lib/project/acl_unittest.py | 190 | ||||
-rw-r--r-- | cli/lib/project/collection.py | 62 | ||||
-rw-r--r-- | cli/lib/project/common.py | 94 | ||||
-rw-r--r-- | cli/lib/project/config.py | 198 | ||||
-rw-r--r-- | cli/lib/project/config_stub.py | 6 | ||||
-rw-r--r-- | cli/lib/project/config_unittest.py | 196 | ||||
-rw-r--r-- | cli/lib/project/dependency.py | 70 | ||||
-rw-r--r-- | cli/lib/project/loader.py | 186 | ||||
-rw-r--r-- | cli/lib/project/pack.py | 791 | ||||
-rw-r--r-- | cli/lib/project/pack_unittest.py | 471 | ||||
-rw-r--r-- | cli/lib/project/packmap.py | 351 | ||||
-rw-r--r-- | cli/lib/project/packmap_stub.py | 16 | ||||
-rw-r--r-- | cli/lib/project/packs.py | 169 | ||||
-rw-r--r-- | cli/lib/project/project_spec.py | 345 | ||||
-rw-r--r-- | cli/lib/project/project_spec_stub.py | 58 | ||||
-rw-r--r-- | cli/lib/project/project_spec_unittest.py | 389 | ||||
-rw-r--r-- | cli/lib/project/target.py | 299 | ||||
-rw-r--r-- | cli/lib/project/target_stub.py | 82 | ||||
-rw-r--r-- | cli/lib/project/targets.py | 95 | ||||
-rw-r--r-- | cli/lib/project/xml_parser.py | 133 |
21 files changed, 2310 insertions, 2274 deletions
diff --git a/cli/lib/project/acl.py b/cli/lib/project/acl.py index 173f523..cb0f44c 100644 --- a/cli/lib/project/acl.py +++ b/cli/lib/project/acl.py @@ -27,194 +27,195 @@ from selinux import file_context class FileAcl(object): - DEFAULTS = { - 'USER': '1000', - 'GROUP': '1000', - 'SELABEL': '', - 'PERMISSIONS': '0400', - 'CAPABILITIES': '0' - } - - def __init__(self, copy): - self._origin = common.Origin() - self._copy = copy - # For now, we default to system. - self._user = self.DEFAULTS['USER'] - self._group = self.DEFAULTS['GROUP'] - self._selabel = self.DEFAULTS['SELABEL'] - self._perms = self.DEFAULTS['PERMISSIONS'] - self._fcaps = self.DEFAULTS['CAPABILITIES'] - # For packs which wrap a Android build, we will often - # want to just use the build systems default values. - # When this is False, fs_config and file_context will - # return empty. - self._override_build = True - # TODO(wad): add more types (b/27848879). - self._file_type = file_context.FILE_TYPE_FILE - default_acl = None - if (copy is not None and - copy.pack is not None and - copy.pack.defaults is not None): - default_acl = copy.pack.defaults.acl - if default_acl is not None: - self._user = default_acl.user - self._group = default_acl.group - self._selabel = default_acl.selabel - self._perms = default_acl.perms - self._fcaps = default_acl.fcaps - - def load(self, ele): - """Loads the FileAcl from a XML element node (set-acl).""" - self._origin = ele.origin.copy() - ele.limit_attribs(['user', 'group', 'selabel', 'perms', 'fcaps']) - # Since all attributes are optional, we can just pull from the - # element attrib. - for key in ele.attrib: - if ele.attrib[key] not in (None, ''): - self.__dict__['_{}'.format(key)] = ele.attrib[key] - if self._perms is not None: - if not re.match('^0[0-7][0-7][0-7]$', self._perms): - raise common.LoadErrorWithOrigin( - self._origin, - '@perms must match ^0[0-7][0-7][0-7]$: {}'.format(self._perms)) - # TODO(wad) Add fcaps content validation. - if self._fcaps != self.DEFAULTS['CAPABILITIES']: - raise common.LoadErrorWithOrigin(self._origin, - '@fcaps support is not yet implemented.') - - @property - def override_build(self): - return self._override_build - - @override_build.setter - def override_build(self, build_def): - self._override_build = build_def - - @property - def user(self): - return self._user - - @user.setter - def user(self, user): - # TODO(wad) http://b/27564772 - try: - self._user = int(user) - except TypeError: - raise ValueError('user must be numeric: {}'.format(user)) - - @property - def group(self): - return self._group - - @group.setter - def group(self, group): - # TODO(wad) http://b/27564772 - try: - self._group = int(group) - except TypeError: - raise ValueError('group must be numeric: {}'.format(group)) - - @property - def perms(self): - return self._perms - - @perms.setter - def perms(self, perms): - try: - self._perms = oct(int(perms, 8)) - except TypeError: - raise ValueError('perms must be octal: {}'.format(perms)) - - @property - def selabel(self): - return self._selabel - - @selabel.setter - def selabel(self, selabel): - self._selabel = selabel - - @property - def fcaps(self): - return self._fcaps - - @fcaps.setter - def fcaps(self, fcaps): - self._fcaps = fcaps - - @property - def copy(self): - return self._copy - - @copy.setter - def copy(self, cpy): - self._copy = cpy - - @property - def file_type(self): - return self._file_type - - @file_type.setter - def file_type(self, ftype): - if ftype not in file_context.FILE_TYPE.keys(): - raise ValueError('ftype must be one of {}: {}'.format( - file_context.FILE_TYPE.keys(), ftype)) - self._file_type = ftype - - def file_context(self, path=None, ftype=None): - """Builds a ASCII SELinux file context entry for the given ACL. - - If the ACL is applied to a GLOB, then path and ftype - should be applied or the glob will be written into the - file_context. - """ - if self.override_build == False: - return '' - path = path or self._copy.dst - if not os.path.isabs(path): - path = os.path.sep + path - ftype = ftype or file_context.FILE_TYPE[self._file_type] - return '{} {} {}'.format(path, ftype, self._selabel) - - def fs_config(self, path=None, binary=False): - """Returns the fs_config entry in ASCII or binary format. - - Args: - path: Optionally override the copy destination. - binary: If True, returns the binary fs_config format. - If False, returns the ASCII fs_config format. - - Note for callers: binary fs_config files will be evaluated - in first match order so it is important to use more specific - paths first. - """ - if self.override_build == False: - return '' - path = path or self._copy.dst - if path.startswith(os.path.sep): - path = path.lstrip('/') - if binary: - # https://android.googlesource.com/platform/system/core/+/master/libcutils/fs_config.c#45 - # struct fs_path_config_from_file { - # uint16_t length; /* header plus prefix */ - # uint16_t mode; - # uint16_t uid; - # uint16_t gid - # uint64_t capabilities; - # char prefix[]; - # } __attribute__((__aligned__(sizeof(uint64_t)))); - path_len = len(path) + 1 # Terminating NUL is required. - total_len = 16 + path_len - # The on-disk format is little endian byte order. - entry = struct.pack( - '<HHHHQ{}s'.format(path_len), total_len, - int(self._perms, 8), int(self._user), - int(self._group), int(self._fcaps), path) - return entry - return '{} {} {} {} capabilities={}'.format( - path, self._user, self._group, self._perms, self._fcaps) - - def __repr__(self): - return ('<set-acl user="{}" group="{}" selabel="{}" ' - 'perms="{}" fcaps="{}"/>'.format( - self._user or '', self._group or '', self._selabel or '', - self._perms or '', self._fcaps or '')) + DEFAULTS = { + 'USER': '1000', + 'GROUP': '1000', + 'SELABEL': '', + 'PERMISSIONS': '0400', + 'CAPABILITIES': '0' + } + + def __init__(self, copy): + self._origin = common.Origin() + self._copy = copy + # For now, we default to system. + self._user = self.DEFAULTS['USER'] + self._group = self.DEFAULTS['GROUP'] + self._selabel = self.DEFAULTS['SELABEL'] + self._perms = self.DEFAULTS['PERMISSIONS'] + self._fcaps = self.DEFAULTS['CAPABILITIES'] + # For packs which wrap a Android build, we will often + # want to just use the build systems default values. + # When this is False, fs_config and file_context will + # return empty. + self._override_build = True + # TODO(wad): add more types (b/27848879). + self._file_type = file_context.FILE_TYPE_FILE + default_acl = None + if (copy is not None and + copy.pack is not None and + copy.pack.defaults is not None): + default_acl = copy.pack.defaults.acl + if default_acl is not None: + self._user = default_acl.user + self._group = default_acl.group + self._selabel = default_acl.selabel + self._perms = default_acl.perms + self._fcaps = default_acl.fcaps + + def load(self, ele): + """Loads the FileAcl from a XML element node (set-acl).""" + self._origin = ele.origin.copy() + ele.limit_attribs(['user', 'group', 'selabel', 'perms', 'fcaps']) + # Since all attributes are optional, we can just pull from the + # element attrib. + for key in ele.attrib: + if ele.attrib[key] not in (None, ''): + self.__dict__['_{}'.format(key)] = ele.attrib[key] + if self._perms is not None: + if not re.match('^0[0-7][0-7][0-7]$', self._perms): + raise common.LoadErrorWithOrigin( + self._origin, + '@perms must match ^0[0-7][0-7][0-7]$: {}'.format( + self._perms)) + # TODO(wad) Add fcaps content validation. + if self._fcaps != self.DEFAULTS['CAPABILITIES']: + raise common.LoadErrorWithOrigin( + self._origin, '@fcaps support is not yet implemented.') + + @property + def override_build(self): + return self._override_build + + @override_build.setter + def override_build(self, build_def): + self._override_build = build_def + + @property + def user(self): + return self._user + + @user.setter + def user(self, user): + # TODO(wad) http://b/27564772 + try: + self._user = int(user) + except TypeError: + raise ValueError('user must be numeric: {}'.format(user)) + + @property + def group(self): + return self._group + + @group.setter + def group(self, group): + # TODO(wad) http://b/27564772 + try: + self._group = int(group) + except TypeError: + raise ValueError('group must be numeric: {}'.format(group)) + + @property + def perms(self): + return self._perms + + @perms.setter + def perms(self, perms): + try: + self._perms = oct(int(perms, 8)) + except TypeError: + raise ValueError('perms must be octal: {}'.format(perms)) + + @property + def selabel(self): + return self._selabel + + @selabel.setter + def selabel(self, selabel): + self._selabel = selabel + + @property + def fcaps(self): + return self._fcaps + + @fcaps.setter + def fcaps(self, fcaps): + self._fcaps = fcaps + + @property + def copy(self): + return self._copy + + @copy.setter + def copy(self, cpy): + self._copy = cpy + + @property + def file_type(self): + return self._file_type + + @file_type.setter + def file_type(self, ftype): + if ftype not in file_context.FILE_TYPE.keys(): + raise ValueError('ftype must be one of {}: {}'.format( + file_context.FILE_TYPE.keys(), ftype)) + self._file_type = ftype + + def file_context(self, path=None, ftype=None): + """Builds a ASCII SELinux file context entry for the given ACL. + + If the ACL is applied to a GLOB, then path and ftype + should be applied or the glob will be written into the + file_context. + """ + if self.override_build == False: + return '' + path = path or self._copy.dst + if not os.path.isabs(path): + path = os.path.sep + path + ftype = ftype or file_context.FILE_TYPE[self._file_type] + return '{} {} {}'.format(path, ftype, self._selabel) + + def fs_config(self, path=None, binary=False): + """Returns the fs_config entry in ASCII or binary format. + + Args: + path: Optionally override the copy destination. + binary: If True, returns the binary fs_config format. + If False, returns the ASCII fs_config format. + + Note for callers: binary fs_config files will be evaluated + in first match order so it is important to use more specific + paths first. + """ + if self.override_build == False: + return '' + path = path or self._copy.dst + if path.startswith(os.path.sep): + path = path.lstrip('/') + if binary: + # https://android.googlesource.com/platform/system/core/+/master/libcutils/fs_config.c#45 + # struct fs_path_config_from_file { + # uint16_t length; /* header plus prefix */ + # uint16_t mode; + # uint16_t uid; + # uint16_t gid + # uint64_t capabilities; + # char prefix[]; + # } __attribute__((__aligned__(sizeof(uint64_t)))); + path_len = len(path) + 1 # Terminating NUL is required. + total_len = 16 + path_len + # The on-disk format is little endian byte order. + entry = struct.pack( + '<HHHHQ{}s'.format(path_len), total_len, + int(self._perms, 8), int(self._user), + int(self._group), int(self._fcaps), path) + return entry + return '{} {} {} {} capabilities={}'.format( + path, self._user, self._group, self._perms, self._fcaps) + + def __repr__(self): + return ('<set-acl user="{}" group="{}" selabel="{}" ' + 'perms="{}" fcaps="{}"/>'.format( + self._user or '', self._group or '', self._selabel or '', + self._perms or '', self._fcaps or '')) diff --git a/cli/lib/project/acl_unittest.py b/cli/lib/project/acl_unittest.py index f570d07..3a3498b 100644 --- a/cli/lib/project/acl_unittest.py +++ b/cli/lib/project/acl_unittest.py @@ -28,98 +28,98 @@ from project.pack import Copy class FileAclTest(unittest.TestCase): - USER = '1000' - GROUP = '1001' - FCAPS = '0' - SELABEL = 'u:object_r:unlabeled:s0' - PERMS = '0600' - XML = '<set-acl perms="{}" user="{}" group="{}" fcaps="{}" selabel="{}"/>' - - def setUp(self): - # Setup a common ACL when needed. - self.acl = FileAcl(None) - xml = self.XML.format( - self.PERMS, self.USER, self.GROUP, self.FCAPS, self.SELABEL) - node = self.node_from_str(xml) - self.acl.load(node) - - def tearDown(self): - pass - - def node_from_str(self, s): - xml = StringIO.StringIO(s) - tree = xml_parser.parse(xml) - return tree.getroot() - - - def test_valid_perms(self): - xml = ('<set-acl perms="0600"/>') - node = self.node_from_str(xml) - acl = FileAcl(None) - acl.load(node) - self.assertIsInstance(acl, FileAcl) - self.assertEqual(acl.perms, '0600') - - def test_invalid_perms(self): - xml = ('<set-acl perms="0609"/>') - node = self.node_from_str(xml) - acl = FileAcl(None) - with self.assertRaises(common.LoadErrorWithOrigin): - acl.load(node) - - def test_valid_attrs(self): - self.assertIsInstance(self.acl, FileAcl) - self.assertEqual(self.acl.perms, self.PERMS) - self.assertEqual(self.acl.user, self.USER) - self.assertEqual(self.acl.group, self.GROUP) - self.assertEqual(self.acl.fcaps, self.FCAPS) - self.assertEqual(self.acl.selabel, self.SELABEL) - - def test_invalid_attrs(self): - xml = ('<set-acl perms="0600" user="1000" group="123" fcaps="0"' - ' custom="value" selabel="u:object_r:unlabeled:s0"/>') - node = self.node_from_str(xml) - acl = FileAcl(None) - with self.assertRaises(common.LoadErrorWithOrigin): - acl.load(node) - - def test_fs_config_ascii(self): - FS_CONFIG_FMT = '{} {} {} {} capabilities={}' - self.acl.copy = Copy(None, dst='/bin/helper') - entry = self.acl.fs_config(binary=False) - expected_entry = FS_CONFIG_FMT.format( - 'bin/helper', self.USER, self.GROUP, self.PERMS, self.FCAPS) - self.assertEqual(entry, expected_entry) - - def test_file_context_ascii(self): - FS_CONFIG_FMT = '{} {} {} {} capabilities={}' - self.acl.copy = Copy(None, dst='/bin/helper') - entry = self.acl.fs_config(binary=False) - expected_entry = FS_CONFIG_FMT.format( - 'bin/helper', self.USER, self.GROUP, self.PERMS, self.FCAPS) - self.assertEqual(entry, expected_entry) - - def test_fs_config_binary(self): - FS_CONFIG_PACK = 'HHHHQ' - self.acl.copy = Copy(None, dst='/bin/helper') - entry = self.acl.fs_config(binary=True) - header = struct.unpack(FS_CONFIG_PACK, entry[:16]) - path = entry[16:] - self.assertEqual(16+len(path), header[0]) - self.assertEqual(int(self.PERMS, 8), header[1]) - self.assertEqual(int(self.USER), header[2]) - self.assertEqual(int(self.GROUP), header[3]) - self.assertEqual(int(self.FCAPS), header[4]) - self.assertEqual(path, 'bin/helper\x00') - - def test_override_build_use(self): - self.acl.copy = Copy(None, dst='/bin/helper') - # Default is True. - self.assertNotEqual(self.acl.fs_config(), '') - self.assertNotEqual(self.acl.file_context(), '') - self.acl.override_build = True - self.assertNotEqual(self.acl.fs_config(), '') - self.assertNotEqual(self.acl.file_context(), '') - self.acl.override_build = False - self.assertEqual(self.acl.fs_config(), '') - self.assertEqual(self.acl.file_context(), '') + USER = '1000' + GROUP = '1001' + FCAPS = '0' + SELABEL = 'u:object_r:unlabeled:s0' + PERMS = '0600' + XML = '<set-acl perms="{}" user="{}" group="{}" fcaps="{}" selabel="{}"/>' + + def setUp(self): + # Setup a common ACL when needed. + self.acl = FileAcl(None) + xml = self.XML.format( + self.PERMS, self.USER, self.GROUP, self.FCAPS, self.SELABEL) + node = self.node_from_str(xml) + self.acl.load(node) + + def tearDown(self): + pass + + def node_from_str(self, s): + xml = StringIO.StringIO(s) + tree = xml_parser.parse(xml) + return tree.getroot() + + + def test_valid_perms(self): + xml = ('<set-acl perms="0600"/>') + node = self.node_from_str(xml) + acl = FileAcl(None) + acl.load(node) + self.assertIsInstance(acl, FileAcl) + self.assertEqual(acl.perms, '0600') + + def test_invalid_perms(self): + xml = ('<set-acl perms="0609"/>') + node = self.node_from_str(xml) + acl = FileAcl(None) + with self.assertRaises(common.LoadErrorWithOrigin): + acl.load(node) + + def test_valid_attrs(self): + self.assertIsInstance(self.acl, FileAcl) + self.assertEqual(self.acl.perms, self.PERMS) + self.assertEqual(self.acl.user, self.USER) + self.assertEqual(self.acl.group, self.GROUP) + self.assertEqual(self.acl.fcaps, self.FCAPS) + self.assertEqual(self.acl.selabel, self.SELABEL) + + def test_invalid_attrs(self): + xml = ('<set-acl perms="0600" user="1000" group="123" fcaps="0"' + ' custom="value" selabel="u:object_r:unlabeled:s0"/>') + node = self.node_from_str(xml) + acl = FileAcl(None) + with self.assertRaises(common.LoadErrorWithOrigin): + acl.load(node) + + def test_fs_config_ascii(self): + FS_CONFIG_FMT = '{} {} {} {} capabilities={}' + self.acl.copy = Copy(None, dst='/bin/helper') + entry = self.acl.fs_config(binary=False) + expected_entry = FS_CONFIG_FMT.format( + 'bin/helper', self.USER, self.GROUP, self.PERMS, self.FCAPS) + self.assertEqual(entry, expected_entry) + + def test_file_context_ascii(self): + FS_CONFIG_FMT = '{} {} {} {} capabilities={}' + self.acl.copy = Copy(None, dst='/bin/helper') + entry = self.acl.fs_config(binary=False) + expected_entry = FS_CONFIG_FMT.format( + 'bin/helper', self.USER, self.GROUP, self.PERMS, self.FCAPS) + self.assertEqual(entry, expected_entry) + + def test_fs_config_binary(self): + FS_CONFIG_PACK = 'HHHHQ' + self.acl.copy = Copy(None, dst='/bin/helper') + entry = self.acl.fs_config(binary=True) + header = struct.unpack(FS_CONFIG_PACK, entry[:16]) + path = entry[16:] + self.assertEqual(16+len(path), header[0]) + self.assertEqual(int(self.PERMS, 8), header[1]) + self.assertEqual(int(self.USER), header[2]) + self.assertEqual(int(self.GROUP), header[3]) + self.assertEqual(int(self.FCAPS), header[4]) + self.assertEqual(path, 'bin/helper\x00') + + def test_override_build_use(self): + self.acl.copy = Copy(None, dst='/bin/helper') + # Default is True. + self.assertNotEqual(self.acl.fs_config(), '') + self.assertNotEqual(self.acl.file_context(), '') + self.acl.override_build = True + self.assertNotEqual(self.acl.fs_config(), '') + self.assertNotEqual(self.acl.file_context(), '') + self.acl.override_build = False + self.assertEqual(self.acl.fs_config(), '') + self.assertEqual(self.acl.file_context(), '') diff --git a/cli/lib/project/collection.py b/cli/lib/project/collection.py index 7bc524d..cf303f9 100644 --- a/cli/lib/project/collection.py +++ b/cli/lib/project/collection.py @@ -19,34 +19,34 @@ class Base(object): - def __init__(self, tag): - self._tag = tag - self._entries = {} - self._origins = [] - - def __iter__(self): - """Enable for entries in collection semantics.""" - return self._entries.values().__iter__() - - @property - def entries(self): - """Returns a dict of name to objects.""" - return self._entries - - @entries.setter - def entries(self, entries): - self._entries = entries - - @property - def origins(self): - """Return the list of Origin objects parsed for this Packs.""" - return self._origins - - @origins.setter - def origins(self, origins): - self._origins = origins - - def __repr__(self): - return '<{}>{}</{}>'.format( - self._tag, ''.join([str(e) for e in self._entries.values()]), - self._tag) + def __init__(self, tag): + self._tag = tag + self._entries = {} + self._origins = [] + + def __iter__(self): + """Enable for entries in collection semantics.""" + return self._entries.values().__iter__() + + @property + def entries(self): + """Returns a dict of name to objects.""" + return self._entries + + @entries.setter + def entries(self, entries): + self._entries = entries + + @property + def origins(self): + """Return the list of Origin objects parsed for this Packs.""" + return self._origins + + @origins.setter + def origins(self, origins): + self._origins = origins + + def __repr__(self): + return '<{}>{}</{}>'.format( + self._tag, ''.join([str(e) for e in self._entries.values()]), + self._tag) diff --git a/cli/lib/project/common.py b/cli/lib/project/common.py index cefb323..40b78fc 100644 --- a/cli/lib/project/common.py +++ b/cli/lib/project/common.py @@ -28,75 +28,75 @@ pathsep = '/' # source files. # TODO(wad,dpursell): Sort out how absolute paths work in this world. def path_to_host(path): - return path.replace(os.sep, pathsep) + return path.replace(os.sep, pathsep) def path_join(*args): - """Naive path join to use the module-local pathsep.""" - path = '' - for c in args: - if (path is not '' and - not path.endswith(pathsep) and - not c.startswith(pathsep)): - path += pathsep - path += c - return path + """Naive path join to use the module-local pathsep.""" + path = '' + for c in args: + if (path is not '' and + not path.endswith(pathsep) and + not c.startswith(pathsep)): + path += pathsep + path += c + return path def basename(path): - return path.split(pathsep)[-1] + return path.split(pathsep)[-1] class Origin(object): - """Used to track the origin location of XML tags.""" - def __init__(self, f='<unknown>', line='-', col='-'): - self.source_file = f - self.line_number = line - self.column_number = col - - def __eq__(self, other): - return ( - isinstance(other, self.__class__) and - other.source_file == self.source_file and - other.line_number == self.line_number and - other.column_number == self.column_number - ) - - def __ne__(self, other): - return not self.__eq__(other) - - def __repr__(self): - return '{}:{}:{}'.format( - self.source_file, self.line_number, self.column_number) - - def copy(self): - """Provide a dedicated helper for copying to encourage - deep copying to avoid keeping references to XML tree - data. - """ - return copy.deepcopy(self) + """Used to track the origin location of XML tags.""" + def __init__(self, f='<unknown>', line='-', col='-'): + self.source_file = f + self.line_number = line + self.column_number = col + + def __eq__(self, other): + return ( + isinstance(other, self.__class__) and + other.source_file == self.source_file and + other.line_number == self.line_number and + other.column_number == self.column_number + ) + + def __ne__(self, other): + return not self.__eq__(other) + + def __repr__(self): + return '{}:{}:{}'.format( + self.source_file, self.line_number, self.column_number) + + def copy(self): + """Provide a dedicated helper for copying to encourage + deep copying to avoid keeping references to XML tree + data. + """ + return copy.deepcopy(self) class Error(error.Error): - pass + pass class LoadError(Error): - pass + pass class LoadErrorWithOrigin(LoadError): - def __init__(self, origin, message): - # Prefix where the error occurred at. - message = '{}: {}'.format(origin, message) - super(LoadErrorWithOrigin, self).__init__(message) + def __init__(self, origin, message): + # Prefix where the error occurred at. + message = '{}: {}'.format(origin, message) + super(LoadErrorWithOrigin, self).__init__(message) class UnknownAttributes(LoadErrorWithOrigin): - pass + pass class MissingAttribute(LoadErrorWithOrigin): - pass + pass class PathConflictError(LoadErrorWithOrigin): - pass + pass diff --git a/cli/lib/project/config.py b/cli/lib/project/config.py index 838a911..480d9e6 100644 --- a/cli/lib/project/config.py +++ b/cli/lib/project/config.py @@ -24,103 +24,103 @@ from project import common class Config(object): - """Config represents project-specific runtime configuration. - - When a given Project is being built, the supporting tools - will have configuration that relates more to the host environment - and less to the creating project artifacts. These may be - output and cache directories or the defaults for commandline - tool behavior. - """ - def __init__(self, base_path=os.getcwd()): - self._version = 1 - self._cache_dir = 'bdk.cache' - self._output_dir = 'bdk.out' - # Fake up an origin using the current directory or a passed - # in base path. - self._origin = common.Origin() - self._origin.source_file = os.path.join(base_path, 'placeholder') - self._default_target = '' - - def _abs_dir(self, p): - if os.path.isabs(p): - return p - # Automatically absolutize relative paths against the origin. - return os.path.join(os.path.dirname(self._origin.source_file), p) - - @property - def cache_dir(self): - """Returns the absolute path dir.""" - return self._abs_dir(self._cache_dir) - - @cache_dir.setter - def cache_dir(self, path): - self._cache_dir = path - - @property - def output_dir(self): - """Returns the absolute path dir.""" - return self._abs_dir(self._output_dir) - - @output_dir.setter - def output_dir(self, path): - self._output_dir = path - - @property - def default_target(self): - return self._default_target - - @default_target.setter - def default_target(self, target_name): - self._default_target = target_name - - @property - def origin(self): - return self._origin - - @origin.setter - def origin(self, o): - self._origin = o.copy() - - def __repr__(self): - return ('<config>' - '<output-dir path="{}"/>' - '<cache-dir path="{}"/>' - '<default target="{}"/></config>').format( - self._output_dir, self._cache_dir, self._default_target) - - @classmethod - def from_element(cls, node): - """Creates a new Config from the @node. - - Args: - node: The ElementTree node a <config> element. - - Returns: - instance of Config - - Raises: - common.LoadErrorWithOrigin + """Config represents project-specific runtime configuration. + + When a given Project is being built, the supporting tools + will have configuration that relates more to the host environment + and less to the creating project artifacts. These may be + output and cache directories or the defaults for commandline + tool behavior. """ - config = cls() - config.origin = node.origin - node.limit_attribs([]) - # Ensure we have a project document. - if node.tag != 'config': - raise common.LoadErrorWithOrigin(node.origin, - 'node not <config>') - - # Parse the dirs - for tag in ('output-dir', 'cache-dir'): - for ele in node.findall(tag): - ele.limit_attribs(['path']) - if 'path' in ele.attrib: - config.__dict__['_' + tag.replace('-', '_')] = ele.get_attrib('path') - - # Grab the default element. - for ele in node.findall('default'): - ele.limit_attribs(['target']) - if 'target' in ele.attrib: - config.default_target = ele.get_attrib('target') - - return config + def __init__(self, base_path=os.getcwd()): + self._version = 1 + self._cache_dir = 'bdk.cache' + self._output_dir = 'bdk.out' + # Fake up an origin using the current directory or a passed + # in base path. + self._origin = common.Origin() + self._origin.source_file = os.path.join(base_path, 'placeholder') + self._default_target = '' + + def _abs_dir(self, p): + if os.path.isabs(p): + return p + # Automatically absolutize relative paths against the origin. + return os.path.join(os.path.dirname(self._origin.source_file), p) + + @property + def cache_dir(self): + """Returns the absolute path dir.""" + return self._abs_dir(self._cache_dir) + + @cache_dir.setter + def cache_dir(self, path): + self._cache_dir = path + + @property + def output_dir(self): + """Returns the absolute path dir.""" + return self._abs_dir(self._output_dir) + + @output_dir.setter + def output_dir(self, path): + self._output_dir = path + + @property + def default_target(self): + return self._default_target + + @default_target.setter + def default_target(self, target_name): + self._default_target = target_name + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, o): + self._origin = o.copy() + + def __repr__(self): + return ('<config>' + '<output-dir path="{}"/>' + '<cache-dir path="{}"/>' + '<default target="{}"/></config>').format( + self._output_dir, self._cache_dir, self._default_target) + + @classmethod + def from_element(cls, node): + """Creates a new Config from the @node. + + Args: + node: The ElementTree node a <config> element. + + Returns: + instance of Config + + Raises: + common.LoadErrorWithOrigin + """ + config = cls() + config.origin = node.origin + node.limit_attribs([]) + # Ensure we have a project document. + if node.tag != 'config': + raise common.LoadErrorWithOrigin(node.origin, 'node not <config>') + + # Parse the dirs + for tag in ('output-dir', 'cache-dir'): + for ele in node.findall(tag): + ele.limit_attribs(['path']) + if 'path' in ele.attrib: + config.__dict__['_' + tag.replace('-', '_')] = ( + ele.get_attrib('path')) + + # Grab the default element. + for ele in node.findall('default'): + ele.limit_attribs(['target']) + if 'target' in ele.attrib: + config.default_target = ele.get_attrib('target') + + return config diff --git a/cli/lib/project/config_stub.py b/cli/lib/project/config_stub.py index b803725..ce2dc22 100644 --- a/cli/lib/project/config_stub.py +++ b/cli/lib/project/config_stub.py @@ -20,6 +20,6 @@ class StubConfig(object): - def __init__(self, default_target='', origin='test_config_origin'): - self.default_target = default_target - self.origin = origin + def __init__(self, default_target='', origin='test_config_origin'): + self.default_target = default_target + self.origin = origin diff --git a/cli/lib/project/config_unittest.py b/cli/lib/project/config_unittest.py index ff0f4c1..6d8eb0c 100644 --- a/cli/lib/project/config_unittest.py +++ b/cli/lib/project/config_unittest.py @@ -28,101 +28,101 @@ from project.config import Config class ConfigTest(unittest.TestCase): - def config_from_str(self, s): - xml = StringIO.StringIO(s) - tree = xml_parser.parse(xml) - config = Config.from_element(tree.getroot()) - return config - - def setUp(self): - pass - - def tearDown(self): - pass - - def test_relative_made_absolute(self): - config = Config() - self.assertEqual(os.path.join(os.getcwd(), 'bdk.cache'), - config.cache_dir) - origin = common.Origin() - origin.source_file = os.path.join('some', 'path', 'file.xml') - config.origin = origin - self.assertEqual(os.path.join('some', 'path', 'bdk.out'), - config.output_dir) - path = 'proj.cache' - xml_s = '<config><cache-dir path="{}"/></config>'.format(path) - config = self.config_from_str(xml_s) - self.assertEqual(path, config.cache_dir) - - def test_absolute_stays_absolute(self): - config = Config() - path = '/foo/bar' - config.output_dir = path - self.assertEqual(path, config.output_dir) - xml_s = '<config><output-dir path="{}"/></config>'.format(path) - config = self.config_from_str(xml_s) - self.assertEqual(path, config.output_dir) - xml_s = '<config><cache-dir path="{}"/></config>'.format(path) - config = self.config_from_str(xml_s) - self.assertEqual(path, config.cache_dir) - - def test_default_cache(self): - config = Config(base_path=os.sep) - self.assertEqual(os.path.join(os.sep, 'bdk.cache'), config.cache_dir) - xml_s = '<config><output-dir path="/some/path"/></config>' - config = self.config_from_str(xml_s) - # No absolutizing happens when the source_file is "<StringIO...>" - # TODO(wad) should we default this (in Origin) to getcwd()? - self.assertEqual('bdk.cache', config.cache_dir) - - def test_default_output(self): - config = Config(base_path=os.sep) - self.assertEqual(os.path.join(os.sep, 'bdk.out'), config.output_dir) - xml_s = '<config></config>' - config = self.config_from_str(xml_s) - # No absolutizing happens when the source_file is "<StringIO...>" - self.assertEqual('bdk.out', config.output_dir) - - def test_default_target_missing(self): - config = Config() - self.assertEqual('', config.default_target) - xml_s = '<config/>' - config = self.config_from_str(xml_s) - self.assertEqual('', config.default_target) - - def test_default_target_supplied(self): - config = Config() - config.default_target = 'foo' - self.assertEqual('foo', config.default_target) - xml_s = '<config><default target="tgt"/></config>' - config = self.config_from_str(xml_s) - self.assertEqual('tgt', config.default_target) - - def test_config_with_attrs(self): - xml_s = '<config some="pig"/>' - with self.assertRaises(common.UnknownAttributes): - self.config_from_str(xml_s) - - def test_config_with_no_children(self): - xml_s = '<config/>' - self.config_from_str(xml_s) - - def test_config_with_unknown_child(self): - xml_s = '<config><random/></config>' - # TODO(wad): currently, extra nodes are _not_ checked. - self.config_from_str(xml_s) - - def test_output_dir_with_other_attr(self): - xml_s = '<config><output-dir path="./" other="bar"/></config>' - with self.assertRaises(common.UnknownAttributes): - self.config_from_str(xml_s) - - def test_cache_dir_with_other_attr(self): - xml_s = '<config><cache-dir path="./" other="bar"/></config>' - with self.assertRaises(common.UnknownAttributes): - self.config_from_str(xml_s) - - def test_default_with_other_attr(self): - xml_s = '<config><default target="xyz" other="bar"/></config>' - with self.assertRaises(common.UnknownAttributes): - self.config_from_str(xml_s) + def config_from_str(self, s): + xml = StringIO.StringIO(s) + tree = xml_parser.parse(xml) + config = Config.from_element(tree.getroot()) + return config + + def setUp(self): + pass + + def tearDown(self): + pass + + def test_relative_made_absolute(self): + config = Config() + self.assertEqual(os.path.join(os.getcwd(), 'bdk.cache'), + config.cache_dir) + origin = common.Origin() + origin.source_file = os.path.join('some', 'path', 'file.xml') + config.origin = origin + self.assertEqual(os.path.join('some', 'path', 'bdk.out'), + config.output_dir) + path = 'proj.cache' + xml_s = '<config><cache-dir path="{}"/></config>'.format(path) + config = self.config_from_str(xml_s) + self.assertEqual(path, config.cache_dir) + + def test_absolute_stays_absolute(self): + config = Config() + path = '/foo/bar' + config.output_dir = path + self.assertEqual(path, config.output_dir) + xml_s = '<config><output-dir path="{}"/></config>'.format(path) + config = self.config_from_str(xml_s) + self.assertEqual(path, config.output_dir) + xml_s = '<config><cache-dir path="{}"/></config>'.format(path) + config = self.config_from_str(xml_s) + self.assertEqual(path, config.cache_dir) + + def test_default_cache(self): + config = Config(base_path=os.sep) + self.assertEqual(os.path.join(os.sep, 'bdk.cache'), config.cache_dir) + xml_s = '<config><output-dir path="/some/path"/></config>' + config = self.config_from_str(xml_s) + # No absolutizing happens when the source_file is "<StringIO...>" + # TODO(wad) should we default this (in Origin) to getcwd()? + self.assertEqual('bdk.cache', config.cache_dir) + + def test_default_output(self): + config = Config(base_path=os.sep) + self.assertEqual(os.path.join(os.sep, 'bdk.out'), config.output_dir) + xml_s = '<config></config>' + config = self.config_from_str(xml_s) + # No absolutizing happens when the source_file is "<StringIO...>" + self.assertEqual('bdk.out', config.output_dir) + + def test_default_target_missing(self): + config = Config() + self.assertEqual('', config.default_target) + xml_s = '<config/>' + config = self.config_from_str(xml_s) + self.assertEqual('', config.default_target) + + def test_default_target_supplied(self): + config = Config() + config.default_target = 'foo' + self.assertEqual('foo', config.default_target) + xml_s = '<config><default target="tgt"/></config>' + config = self.config_from_str(xml_s) + self.assertEqual('tgt', config.default_target) + + def test_config_with_attrs(self): + xml_s = '<config some="pig"/>' + with self.assertRaises(common.UnknownAttributes): + self.config_from_str(xml_s) + + def test_config_with_no_children(self): + xml_s = '<config/>' + self.config_from_str(xml_s) + + def test_config_with_unknown_child(self): + xml_s = '<config><random/></config>' + # TODO(wad): currently, extra nodes are _not_ checked. + self.config_from_str(xml_s) + + def test_output_dir_with_other_attr(self): + xml_s = '<config><output-dir path="./" other="bar"/></config>' + with self.assertRaises(common.UnknownAttributes): + self.config_from_str(xml_s) + + def test_cache_dir_with_other_attr(self): + xml_s = '<config><cache-dir path="./" other="bar"/></config>' + with self.assertRaises(common.UnknownAttributes): + self.config_from_str(xml_s) + + def test_default_with_other_attr(self): + xml_s = '<config><default target="xyz" other="bar"/></config>' + with self.assertRaises(common.UnknownAttributes): + self.config_from_str(xml_s) diff --git a/cli/lib/project/dependency.py b/cli/lib/project/dependency.py index 36b73f5..f5115e0 100644 --- a/cli/lib/project/dependency.py +++ b/cli/lib/project/dependency.py @@ -19,54 +19,54 @@ from project import common class Error(common.Error): - """The base class for any dependency-related error.""" + """The base class for any dependency-related error.""" class UnsatisfiedVirtualPackError(Error): - """Raised when a dependency could be resolved globally, but isn't for - the given dependency tree. - """ - def __init__(self, deps): - # TODO(wad) Sort by origin - msg = '\n'.join([str(d) for d in deps]) - super(UnsatisfiedVirtualPackError, self).__init__(msg) + """Raised when a dependency could be resolved globally, but isn't for + the given dependency tree. + """ + def __init__(self, deps): + # TODO(wad) Sort by origin + msg = '\n'.join([str(d) for d in deps]) + super(UnsatisfiedVirtualPackError, self).__init__(msg) class UndefinedPackError(Error): - def __init__(self, undef): - # TODO(wad) Sort by origin - msg = '\n'.join([str(d) for d in undef]) - super(UndefinedPackError, self).__init__(msg) + def __init__(self, undef): + # TODO(wad) Sort by origin + msg = '\n'.join([str(d) for d in undef]) + super(UndefinedPackError, self).__init__(msg) class Dependency(object): - pass + pass class Virtual(Dependency): - def __init__(self, name, root, required_by, candidates): - self._name = name - self._root = root - self._required_by = required_by - self._candidates = candidates + def __init__(self, name, root, required_by, candidates): + self._name = name + self._root = root + self._required_by = required_by + self._candidates = candidates - def __repr__(self): - return ('Pack(s) {} requires virtual pack "{}" is unsatisfied in ' - 'the dependency tree for pack "{}". The following packs ' - 'may satisfy the requirement: {}'.format( - ', '.join(['"{}"'.format(r) for r in self._required_by]), - self._name, self._root, self._candidates)) + def __repr__(self): + return ('Pack(s) {} requires virtual pack "{}" is unsatisfied in ' + 'the dependency tree for pack "{}". The following packs ' + 'may satisfy the requirement: {}'.format( + ', '.join(['"{}"'.format(r) for r in self._required_by]), + self._name, self._root, self._candidates)) class Undefined(Dependency): - def __init__(self, name, required_by): - self._name = name - self._required_by = required_by - - def __repr__(self): - s = 'Undefined pack "{}" is required by '.format(self._name) - callers = [] - for p in self._required_by: - callers += ['pack "{}" defined at "{}"'.format(p.uid, p.origin)] - s += '{}.'.format(', '.join(callers)) - return s + def __init__(self, name, required_by): + self._name = name + self._required_by = required_by + + def __repr__(self): + s = 'Undefined pack "{}" is required by '.format(self._name) + callers = [] + for p in self._required_by: + callers += ['pack "{}" defined at "{}"'.format(p.uid, p.origin)] + s += '{}.'.format(', '.join(callers)) + return s diff --git a/cli/lib/project/loader.py b/cli/lib/project/loader.py index ce825bf..c82b66c 100644 --- a/cli/lib/project/loader.py +++ b/cli/lib/project/loader.py @@ -22,104 +22,106 @@ from project import xml_parser class PathElementLoader(object): - """Processes an XML element node with either a 'path' attribute - or child nodes. All attributes in the attrib_rollover list - will be propagated down to the final returned root node. - """ - def __init__(self, tag, rollover_attribs): - self._tag = tag - self._rollover_names = rollover_attribs - self._rollover_attribs = {} - # List of origins for any parsed @tag - # nodes. This is only a list because - # @tag may load a different @tag node. - self._origins = [] + """Processes an XML element node with either a 'path' attribute + or child nodes. All attributes in the attrib_rollover list + will be propagated down to the final returned root node. + """ + def __init__(self, tag, rollover_attribs): + self._tag = tag + self._rollover_names = rollover_attribs + self._rollover_attribs = {} + # List of origins for any parsed @tag + # nodes. This is only a list because + # @tag may load a different @tag node. + self._origins = [] - @property - def origins(self): - """Return the list of Origin objects parsed for this loader.""" - return self._origins + @property + def origins(self): + """Return the list of Origin objects parsed for this loader.""" + return self._origins - def load(self, **source): - """Populates the instance from an XML file or element. + def load(self, **source): + """Populates the instance from an XML file or element. - Dereferences elements that may include other files returning - the final defined element. + Dereferences elements that may include other files returning + the final defined element. - Args: - **source: May have one of the following keys set: - - 'path': The path (str) to a file to open, read, and parse. - - 'file': A file-compatible object to read from for parsing. - - 'element': Provides the root element node to walk. + Args: + **source: May have one of the following keys set: + - 'path': The path (str) to a file to open, read, and parse. + - 'file': A file-compatible object to read from for parsing. + - 'element': Provides the root element node to walk. - Returns: - Actual root node with overridden rollover attributes and - a new 'old_attribs' attribute containing the most recent root node's - original values. + Returns: + Actual root node with overridden rollover attributes and + a new 'old_attribs' attribute containing the most recent root node's + original values. - Raises - LoadError: LoadError, or a subclass, will be raised if an error occurs - loading and validating content from the XML. - """ - ALLOWED_KEYS = ('path', 'file', 'element') - if len(source) != 1 or source.keys()[0] not in ALLOWED_KEYS: - raise ValueError( - 'Exactly one of {} must be supplied: {}'.format(ALLOWED_KEYS, source)) - if 'element' in source: - return self._load_element(source['element']) + Raises + LoadError: LoadError, or a subclass, will be raised if an error + occurs loading and validating content from the XML. + """ + ALLOWED_KEYS = ('path', 'file', 'element') + if len(source) != 1 or source.keys()[0] not in ALLOWED_KEYS: + raise ValueError( + 'Exactly one of {} must be supplied: {}'.format(ALLOWED_KEYS, + source)) + if 'element' in source: + return self._load_element(source['element']) - src = '' - if 'path' in source: - src = os.path.abspath(source['path']) - elif 'file' in source: - src = source['file'] - root = self._load_file(src) - if root is None: - raise common.LoadError( - 'Failed to access and read file: {}'.format(src)) - return root + src = '' + if 'path' in source: + src = os.path.abspath(source['path']) + elif 'file' in source: + src = source['file'] + root = self._load_file(src) + if root is None: + raise common.LoadError( + 'Failed to access and read file: {}'.format(src)) + return root - def _load_file(self, f): - seen = [x for x in self._origins if x.source_file == f] - if len(seen): - # Break cycles: A -> B -> A - print 'Warning: path "{}" included more than once.'.format(f) - return None - try: - tree = xml_parser.parse(f) - except IOError: - return None - root = tree.getroot() - if root.tag != self._tag: - # pylint: disable=no-member - raise common.LoadErrorWithOrigin( - root.origin, - 'Included file "{}" root node is not a <{}>'.format(f, self._tag)) - return self._load_element(root) + def _load_file(self, f): + seen = [x for x in self._origins if x.source_file == f] + if len(seen): + # Break cycles: A -> B -> A + print 'Warning: path "{}" included more than once.'.format(f) + return None + try: + tree = xml_parser.parse(f) + except IOError: + return None + root = tree.getroot() + if root.tag != self._tag: + # pylint: disable=no-member + raise common.LoadErrorWithOrigin( + root.origin, + 'Included file "{}" root node is not a <{}>'.format(f, + self._tag)) + return self._load_element(root) - def _load_element(self, root): - self._origins += [root.origin.copy()] - # Overwrite any attribs from rollover_attribs. - root.old_attrib = {} - for attr in self._rollover_attribs: - if attr in root.attrib: - root.old_attrib[attr] = root.attrib[attr] - root.attrib[attr] = self._rollover_attribs[attr] - # Copy out any attribs that should rollover. - for attr in root.attrib: - if attr in self._rollover_names: - self._rollover_attribs[attr] = root.attrib[attr] - if 'path' in root.attrib: - path = root.get_attrib('path') - # Resolve relative paths using the including file's path. - if path[0] != '/': - caller = root.origin.source_file - path = os.path.join(os.path.dirname(caller), path) - # Populate internal state from the given path. - new_root = self._load_file(path) - if new_root is None: - raise common.LoadErrorWithOrigin( - root.origin, - 'Unable to load <{}> from file: {}'.format(self._tag, path)) - return new_root - return root + def _load_element(self, root): + self._origins += [root.origin.copy()] + # Overwrite any attribs from rollover_attribs. + root.old_attrib = {} + for attr in self._rollover_attribs: + if attr in root.attrib: + root.old_attrib[attr] = root.attrib[attr] + root.attrib[attr] = self._rollover_attribs[attr] + # Copy out any attribs that should rollover. + for attr in root.attrib: + if attr in self._rollover_names: + self._rollover_attribs[attr] = root.attrib[attr] + if 'path' in root.attrib: + path = root.get_attrib('path') + # Resolve relative paths using the including file's path. + if path[0] != '/': + caller = root.origin.source_file + path = os.path.join(os.path.dirname(caller), path) + # Populate internal state from the given path. + new_root = self._load_file(path) + if new_root is None: + raise common.LoadErrorWithOrigin( + root.origin, + 'Unable to load <{}> from file: {}'.format(self._tag, path)) + return new_root + return root diff --git a/cli/lib/project/pack.py b/cli/lib/project/pack.py index 83e766a..ab0db05 100644 --- a/cli/lib/project/pack.py +++ b/cli/lib/project/pack.py @@ -26,414 +26,417 @@ WILDCARD = '*' class Pack(object): - """Pack objects represent the <pack> element.""" - def __init__(self, namespace, name): - self._namespace = namespace - self._name = name - self._depends = {'requires':[], 'provides':[]} - self._copies = [] - self._configs = [] - self._defaults = DefaultCopy() - self._origin = common.Origin() - - def _alias_puidlist(self, old_ns, new_ns, puidlist): - """Takes a list of pack ids and changes their prefix in a new list.""" - new_list = [] - for puid in puidlist: - if puid.startswith('{}.'.format(old_ns)): - new_list += ['{}{}'.format(new_ns, puid[len(old_ns):])] - else: - new_list += [puid] - return new_list - - def alias(self, new_ns): - """Swap out the namespace prefix in all absolute ids.""" - self._depends['requires'] = self._alias_puidlist( - self._namespace, new_ns, self._depends['requires']) - self._depends['provides'] = self._alias_puidlist( - self._namespace, new_ns, self._depends['provides']) - self._namespace = new_ns - - @property - def namespace(self): - return self._namespace - - @property - def origin(self): - """Returns string indicating where the pack was defined.""" - return self._origin - - @property - def uid(self): - """Returns the fully qualified unique pack name""" - return '{}.{}'.format(self._namespace, self._name) - - @property - def name(self): - """Returns the namespace-local pack name.""" - return self._name - - @property - def requires(self): - """Returns a list of required fully qualified pack unique names.""" - return self._depends['requires'] - - @property - def provides(self): - """Returns a list of provided fully qualified pack virtual names.""" - return self._depends['provides'] - - def add_provides(self, virtual_name): - # Duplicates are ignored. - if virtual_name in self._depends['provides']: - return - self._depends['provides'].append(virtual_name) - - @property - def defaults(self): - """Returns the DefaultCopy node if defined.""" - return self._defaults - - @defaults.setter - def defaults(self, default_copy): - self._defaults = default_copy - - @property - def copies(self): - """Returns the list of child Copy objects.""" - return self._copies - - def add_copy(self, copy): - self._copies.append(copy) - - @property - def configs(self): - """Returns the list of child Config objects.""" - return self._configs - - def __repr__(self): - s = '<pack name="{}">{}{}{}{}{}</pack>'.format( - self._name, - ''.join(['<provides pack="{}"/>'.format(p) - for p in self._depends['provides']]), - ''.join(['<requires pack="{}"/>'.format(p) - for p in self._depends['requires']]), - ''.join([str(c) for c in self._copies]), - self._configs or '', - self._defaults - ) - return s - - def load(self, ele): - """Loads the Pack from a XML element node.""" - ele.limit_attribs(['name']) - # Override any pre-defined name. - self._name = ele.get_attrib('name') - self._origin = ele.origin.copy() - for child in ele.findall('defaults'): - self._defaults.load(child) - for pack_op in self._depends: - for child in ele.findall(pack_op): - child.limit_attribs(['pack']) - n = child.get_attrib('pack') - if '.' not in n: - raise common.LoadErrorWithOrigin( - child.origin, - '<{}> must supply fully qualified pack names: {}'.format( - pack_op, n)) - self._depends[pack_op].append(n) - for child in ele.findall('copy'): - c = Copy(self) - c.load(child) - self._copies.append(c) - for child in ele.findall('config'): - c = Config(self) - c.load(child) - self._configs.append(c) + """Pack objects represent the <pack> element.""" + def __init__(self, namespace, name): + self._namespace = namespace + self._name = name + self._depends = {'requires':[], 'provides':[]} + self._copies = [] + self._configs = [] + self._defaults = DefaultCopy() + self._origin = common.Origin() + + def _alias_puidlist(self, old_ns, new_ns, puidlist): + """Takes a list of pack ids and changes their prefix in a new list.""" + new_list = [] + for puid in puidlist: + if puid.startswith('{}.'.format(old_ns)): + new_list += ['{}{}'.format(new_ns, puid[len(old_ns):])] + else: + new_list += [puid] + return new_list + + def alias(self, new_ns): + """Swap out the namespace prefix in all absolute ids.""" + self._depends['requires'] = self._alias_puidlist( + self._namespace, new_ns, self._depends['requires']) + self._depends['provides'] = self._alias_puidlist( + self._namespace, new_ns, self._depends['provides']) + self._namespace = new_ns + + @property + def namespace(self): + return self._namespace + + @property + def origin(self): + """Returns string indicating where the pack was defined.""" + return self._origin + + @property + def uid(self): + """Returns the fully qualified unique pack name""" + return '{}.{}'.format(self._namespace, self._name) + + @property + def name(self): + """Returns the namespace-local pack name.""" + return self._name + + @property + def requires(self): + """Returns a list of required fully qualified pack unique names.""" + return self._depends['requires'] + + @property + def provides(self): + """Returns a list of provided fully qualified pack virtual names.""" + return self._depends['provides'] + + def add_provides(self, virtual_name): + # Duplicates are ignored. + if virtual_name in self._depends['provides']: + return + self._depends['provides'].append(virtual_name) + + @property + def defaults(self): + """Returns the DefaultCopy node if defined.""" + return self._defaults + + @defaults.setter + def defaults(self, default_copy): + self._defaults = default_copy + + @property + def copies(self): + """Returns the list of child Copy objects.""" + return self._copies + + def add_copy(self, copy): + self._copies.append(copy) + + @property + def configs(self): + """Returns the list of child Config objects.""" + return self._configs + + def __repr__(self): + s = '<pack name="{}">{}{}{}{}{}</pack>'.format( + self._name, + ''.join(['<provides pack="{}"/>'.format(p) + for p in self._depends['provides']]), + ''.join(['<requires pack="{}"/>'.format(p) + for p in self._depends['requires']]), + ''.join([str(c) for c in self._copies]), + self._configs or '', + self._defaults + ) + return s + + def load(self, ele): + """Loads the Pack from a XML element node.""" + ele.limit_attribs(['name']) + # Override any pre-defined name. + self._name = ele.get_attrib('name') + self._origin = ele.origin.copy() + for child in ele.findall('defaults'): + self._defaults.load(child) + for pack_op in self._depends: + for child in ele.findall(pack_op): + child.limit_attribs(['pack']) + n = child.get_attrib('pack') + if '.' not in n: + raise common.LoadErrorWithOrigin( + child.origin, + ('<{}> must supply fully qualified pack names: ' + '{}'.format(pack_op, n))) + self._depends[pack_op].append(n) + for child in ele.findall('copy'): + c = Copy(self) + c.load(child) + self._copies.append(c) + for child in ele.findall('config'): + c = Config(self) + c.load(child) + self._configs.append(c) class CopyType(object): - DIR = 1 - FILE = 2 - GLOB = 3 - @staticmethod - def get(path): - if path.endswith(os.sep): - return CopyType.DIR - if path.endswith(WILDCARD): - return CopyType.GLOB - return CopyType.FILE + DIR = 1 + FILE = 2 + GLOB = 3 + @staticmethod + def get(path): + if path.endswith(os.sep): + return CopyType.DIR + if path.endswith(WILDCARD): + return CopyType.GLOB + return CopyType.FILE class DefaultCopy(object): - def __init__(self): - self._origin = common.Origin() - self._dst = None - self._acl = acl.FileAcl(None) - - def load(self, ele): - ele.limit_attribs([]) - copies = ele.findall('copy') - if len(copies) == 0: - return - if len(copies) != 1: - raise common.LoadErrorWithOrigin( - ele.origin, 'Only one copy element may be defined in defaults.') - ele = copies[0] - ele.limit_attribs(['to']) - # TODO(wad) Here and elsewhere, ensure attrib only - # has keys that are explicitly supported. - self._origin = ele.origin.copy() - if 'to' in ele.attrib: - self._dst = ele.get_attrib('to') - if CopyType.get(self._dst) != CopyType.DIR: - raise common.LoadErrorWithOrigin( - self._origin, - ('default copy destinations must be directories, suffixed ' - 'by a {}'.format(common.pathsep))) - acls = ele.findall('set-acl') - if len(acls) > 1: - raise common.LoadErrorWithOrigin(ele.origin, - 'Only one set-acl default may be set') - if len(acls): - self._acl.load(acls[0]) - - @property - def dst(self): - return self._dst - - @property - def acl(self): - return self._acl - - def __repr__(self): - return '<defaults><copy to="{}">{}</copy></defaults>'.format( - self._dst or '', self._acl) + def __init__(self): + self._origin = common.Origin() + self._dst = None + self._acl = acl.FileAcl(None) + + def load(self, ele): + ele.limit_attribs([]) + copies = ele.findall('copy') + if len(copies) == 0: + return + if len(copies) != 1: + raise common.LoadErrorWithOrigin( + ele.origin, 'Only one copy element may be defined in defaults.') + ele = copies[0] + ele.limit_attribs(['to']) + # TODO(wad) Here and elsewhere, ensure attrib only + # has keys that are explicitly supported. + self._origin = ele.origin.copy() + if 'to' in ele.attrib: + self._dst = ele.get_attrib('to') + if CopyType.get(self._dst) != CopyType.DIR: + raise common.LoadErrorWithOrigin( + self._origin, + ('default copy destinations must be directories, suffixed ' + 'by a {}'.format(common.pathsep))) + acls = ele.findall('set-acl') + if len(acls) > 1: + raise common.LoadErrorWithOrigin( + ele.origin, 'Only one set-acl default may be set') + if len(acls): + self._acl.load(acls[0]) + + @property + def dst(self): + return self._dst + + @property + def acl(self): + return self._acl + + def __repr__(self): + return '<defaults><copy to="{}">{}</copy></defaults>'.format( + self._dst or '', self._acl) class Copy(object): - def __init__(self, pack, - dst='', dst_type=CopyType.FILE, - src='', src_type=CopyType.FILE): - self._pack = pack - self._dst = dst - self._dst_type = dst_type - # Set the default "to" if the pack has none. - if pack is not None and pack.defaults is not None: - if pack.defaults.dst is not None: - self._dst = pack.defaults.dst + def __init__(self, pack, dst='', dst_type=CopyType.FILE, src='', + src_type=CopyType.FILE): + self._pack = pack + self._dst = dst + self._dst_type = dst_type + # Set the default "to" if the pack has none. + if pack is not None and pack.defaults is not None: + if pack.defaults.dst is not None: + self._dst = pack.defaults.dst + self._dst_type = CopyType.get(self._dst) + self._src = src + self._src_type = src_type + self._recurse = False + self._acl = acl.FileAcl(self) + self._origin = common.Origin() + + @property + def pack(self): + return self._pack + + @property + def origin(self): + return self._origin + + def load(self, ele): + """Loads the Copy from a XML element node (copy).""" + ele.limit_attribs(['to', 'from', 'recurse']) + # Always get to from the element. + if 'to' in ele.attrib: + self._dst = ele.get_attrib('to') + # Fail if there is no 'to' default or defined 'to'. + if self._dst == '': + self._dst = ele.get_attrib('to') + self._dst_type = CopyType.get(self._dst) - self._src = src - self._src_type = src_type - self._recurse = False - self._acl = acl.FileAcl(self) - self._origin = common.Origin() - - @property - def pack(self): - return self._pack - - @property - def origin(self): - return self._origin - - def load(self, ele): - """Loads the Copy from a XML element node (copy).""" - ele.limit_attribs(['to', 'from', 'recurse']) - # Always get to from the element. - if 'to' in ele.attrib: - self._dst = ele.get_attrib('to') - # Fail if there is no 'to' default or defined 'to'. - if self._dst == '': - self._dst = ele.get_attrib('to') - - self._dst_type = CopyType.get(self._dst) - self._src = ele.get_attrib('from') - self._src_type = CopyType.get(self._src) - recurse = (ele.attrib.get('recurse') or 'false').lower() - self._origin = ele.origin.copy() - if recurse == 'true': - self._recurse = True - elif recurse == 'false': - self._recurse = False - else: - raise common.LoadErrorWithOrigin( - self._origin, '<copy> recurse element not "true" or "false"') - self._recurse = recurse == 'true' or False - - acls = ele.findall('set-acl') - if len(acls) > 1: - raise common.LoadErrorWithOrigin( - ele.origin, 'Only one <set-acl> element is allowed per <copy>') - if len(acls): - self._acl.load(acls[0]) - self._reconcile_paths() - - def _reconcile_paths(self): - if not self._dst.startswith(common.pathsep): - raise common.LoadErrorWithOrigin( - self._origin, - '<copy> destinations must be absolute (start with a {}): {}'.format( - common.pathsep, self._dst)) - if self._recurse: - # Ensure recursive destinations are paths and require a trailing slash. - # This is pedantic, but it is better to start explicit. - if not self._dst_type == CopyType.DIR: - raise common.LoadErrorWithOrigin( - self._origin, - '<copy> specifies recursion but the destination "{}" ' - 'does not have a trailing path separator'.format(self._dst)) - # Similarly, recursive sources must be labeled as paths using trailing - # slashes or end with a wildcard (*) allowing a path to be copied - # or the contents of a path. - if self._src_type == CopyType.FILE: - raise common.LoadErrorWithOrigin( - self._origin, - '<copy> specifies recursion but the source "{}" ' - 'does not have a trailing path separator or wildcard'.format( - self._src)) - # If we're copying the path, we could translate it into a more - # specific glob here, but there is no benefit using normal python - # helpers. - - # For file copies into a path, compute the final path. - if not self._recurse: - if self._dst_type == CopyType.DIR and self._src_type == CopyType.FILE: - # foo/bar.txt -> /system/bin/ becomes /system/bin/bar.txt - self._dst = common.path_join(self._dst, common.basename(self._src)) - self._dst_type = CopyType.FILE - elif self._dst_type == CopyType.DIR and self._src_type == CopyType.DIR: - # foo/bar/ -> /system/blah becomes /system/blah/bar/ - # (GLOBs are used to copy contents to the same name.) - self._dst = common.path_join( - self._dst, common.basename(self._src.rstrip(common.pathsep))) - elif (self._src_type == CopyType.GLOB and not - self._dst_type == CopyType.DIR): - raise common.LoadErrorWithOrigin( - self._origin, - '<copy> source "{}" uses a wildcard but the ' - 'destination "{}" does not have a trailing path separator'.format( - self._src, self._dst)) - - # Absolutize the sources based on the defining file. - # This also means that sources must use host separators. - # TODO(wad) Consider allowing a base to be set by Packs on - # inclusion of a file. - self._src = common.path_to_host(self._src) - if not os.path.isabs(self._src): - base_path = os.path.dirname(self._origin.source_file) - self._src = os.path.abspath(os.path.join(base_path, self._src)) - - # Note, if we end up creating an image root, then we can also absolutize - # the destinations (excepting wildcards). - - @property - def src(self): - """Returns the path on the caller's client.""" - return self._src - - @src.setter - def src(self, src): - self._src = src - - @property - def dst(self): - return self._dst - - @dst.setter - def dst(self, dst): - self._dst = dst - - @property - def src_type(self): - return self._src_type - - @property - def dst_type(self): - return self._dst_type - - @src_type.setter - def src_type(self, t): - self._src_type = t - - @dst_type.setter - def dst_type(self, t): - self._dst_type = t - - @property - def recurse(self): - return self._recurse - - @recurse.setter - def recurse(self, r): - self._recurse = r - - @property - def acl(self): - return self._acl - - @acl.setter - def acl(self, acl_): - self._acl = acl_ - - def __repr__(self): - return '<copy to="{}" from="{}" recurse="{}">{}</copy>'.format( - self._dst, self._src, self._recurse, self._acl) + self._src = ele.get_attrib('from') + self._src_type = CopyType.get(self._src) + recurse = (ele.attrib.get('recurse') or 'false').lower() + self._origin = ele.origin.copy() + if recurse == 'true': + self._recurse = True + elif recurse == 'false': + self._recurse = False + else: + raise common.LoadErrorWithOrigin( + self._origin, '<copy> recurse element not "true" or "false"') + self._recurse = recurse == 'true' or False + + acls = ele.findall('set-acl') + if len(acls) > 1: + raise common.LoadErrorWithOrigin( + ele.origin, 'Only one <set-acl> element is allowed per <copy>') + if len(acls): + self._acl.load(acls[0]) + self._reconcile_paths() + + def _reconcile_paths(self): + if not self._dst.startswith(common.pathsep): + raise common.LoadErrorWithOrigin( + self._origin, + ('<copy> destinations must be absolute (start with a {}): ' + '{}'.format(common.pathsep, self._dst))) + if self._recurse: + # Ensure recursive destinations are paths and require a trailing + # slash. This is pedantic, but it is better to start explicit. + if not self._dst_type == CopyType.DIR: + raise common.LoadErrorWithOrigin( + self._origin, + '<copy> specifies recursion but the destination "{}" ' + 'does not have a trailing path separator'.format(self._dst)) + # Similarly, recursive sources must be labeled as paths using + # trailing slashes or end with a wildcard (*) allowing a path to be + # copied or the contents of a path. + if self._src_type == CopyType.FILE: + raise common.LoadErrorWithOrigin( + self._origin, + '<copy> specifies recursion but the source "{}" ' + 'does not have a trailing path separator or ' + 'wildcard'.format(self._src)) + # If we're copying the path, we could translate it into a more + # specific glob here, but there is no benefit using normal python + # helpers. + + # For file copies into a path, compute the final path. + if not self._recurse: + if (self._dst_type == CopyType.DIR + and self._src_type == CopyType.FILE): + # foo/bar.txt -> /system/bin/ becomes /system/bin/bar.txt + self._dst = common.path_join(self._dst, + common.basename(self._src)) + self._dst_type = CopyType.FILE + elif (self._dst_type == CopyType.DIR + and self._src_type == CopyType.DIR): + # foo/bar/ -> /system/blah becomes /system/blah/bar/ + # (GLOBs are used to copy contents to the same name.) + self._dst = common.path_join( + self._dst, + common.basename(self._src.rstrip(common.pathsep))) + elif (self._src_type == CopyType.GLOB + and not self._dst_type == CopyType.DIR): + raise common.LoadErrorWithOrigin( + self._origin, + '<copy> source "{}" uses a wildcard but the destination ' + '"{}" does not have a trailing path separator'.format( + self._src, self._dst)) + + # Absolutize the sources based on the defining file. + # This also means that sources must use host separators. + # TODO(wad) Consider allowing a base to be set by Packs on + # inclusion of a file. + self._src = common.path_to_host(self._src) + if not os.path.isabs(self._src): + base_path = os.path.dirname(self._origin.source_file) + self._src = os.path.abspath(os.path.join(base_path, self._src)) + + # Note, if we end up creating an image root, then we can also absolutize + # the destinations (excepting wildcards). + + @property + def src(self): + """Returns the path on the caller's client.""" + return self._src + + @src.setter + def src(self, src): + self._src = src + + @property + def dst(self): + return self._dst + + @dst.setter + def dst(self, dst): + self._dst = dst + + @property + def src_type(self): + return self._src_type + + @property + def dst_type(self): + return self._dst_type + + @src_type.setter + def src_type(self, t): + self._src_type = t + + @dst_type.setter + def dst_type(self, t): + self._dst_type = t + + @property + def recurse(self): + return self._recurse + + @recurse.setter + def recurse(self, r): + self._recurse = r + + @property + def acl(self): + return self._acl + + @acl.setter + def acl(self, acl_): + self._acl = acl_ + + def __repr__(self): + return '<copy to="{}" from="{}" recurse="{}">{}</copy>'.format( + self._dst, self._src, self._recurse, self._acl) class ConfigType(object): - UNKNOWN = 0 - KERNEL_FRAGMENT = 1 - SELINUX_POLICY = 2 - NAMES = {'kernel-fragment': KERNEL_FRAGMENT, - 'sepolicy': SELINUX_POLICY} + UNKNOWN = 0 + KERNEL_FRAGMENT = 1 + SELINUX_POLICY = 2 + NAMES = {'kernel-fragment': KERNEL_FRAGMENT, + 'sepolicy': SELINUX_POLICY} - @staticmethod - def get(type_name): - if type_name in ConfigType.NAMES: - return ConfigType.NAMES[type_name] - return ConfigType.UNKNOWN + @staticmethod + def get(type_name): + if type_name in ConfigType.NAMES: + return ConfigType.NAMES[type_name] + return ConfigType.UNKNOWN class Config(object): - def __init__(self, pack): - self._pack = pack - self._path = () - self._type = ConfigType.UNKNOWN - self._origin = common.Origin() - - @property - def pack(self): - return self._pack - - def load(self, ele): - """Loads the Config from a XML element node (config).""" - ele.limit_attribs(['path', 'type']) - self._origin = ele.origin.copy() - self._path = common.path_to_host(ele.get_attrib('path')) - if not isinstance(self._path, basestring): - raise common.LoadErrorWithOrigin(ele.origin, - 'Failed to parse "path" attribute') - - # Anchor a relative path to the origin's location. - if not os.path.isabs(self._path): - base_path = os.path.dirname(self._origin.source_file) - self._path = os.path.abspath(os.path.join(base_path, self._path)) - if self._path.endswith(os.sep): - raise common.LoadErrorWithOrigin( - self._origin, - '@path must specify a file and not a directory: {}'.format( - self._path)) - self._type = ConfigType.get(ele.get_attrib('type')) - if self._type == ConfigType.UNKNOWN: - raise common.LoadErrorWithOrigin( - self._origin, - '@type must be one of {}'.format(ConfigType.NAMES.keys())) - - @property - def path(self): - return self._path - - def __repr__(self): - return '<config type="{}" path="{}"'.format( - self._type, self._path) + def __init__(self, pack): + self._pack = pack + self._path = () + self._type = ConfigType.UNKNOWN + self._origin = common.Origin() + + @property + def pack(self): + return self._pack + + def load(self, ele): + """Loads the Config from a XML element node (config).""" + ele.limit_attribs(['path', 'type']) + self._origin = ele.origin.copy() + self._path = common.path_to_host(ele.get_attrib('path')) + if not isinstance(self._path, basestring): + raise common.LoadErrorWithOrigin(ele.origin, + 'Failed to parse "path" attribute') + + # Anchor a relative path to the origin's location. + if not os.path.isabs(self._path): + base_path = os.path.dirname(self._origin.source_file) + self._path = os.path.abspath(os.path.join(base_path, self._path)) + if self._path.endswith(os.sep): + raise common.LoadErrorWithOrigin( + self._origin, + '@path must specify a file and not a directory: {}'.format( + self._path)) + self._type = ConfigType.get(ele.get_attrib('type')) + if self._type == ConfigType.UNKNOWN: + raise common.LoadErrorWithOrigin( + self._origin, + '@type must be one of {}'.format(ConfigType.NAMES.keys())) + + @property + def path(self): + return self._path + + def __repr__(self): + return '<config type="{}" path="{}"'.format( + self._type, self._path) diff --git a/cli/lib/project/pack_unittest.py b/cli/lib/project/pack_unittest.py index 2fa80dd..ba02866 100644 --- a/cli/lib/project/pack_unittest.py +++ b/cli/lib/project/pack_unittest.py @@ -34,36 +34,36 @@ from project.pack import Pack class PackTest(unittest.TestCase): - def pack_from_str(self, ns, name, s): - pack_xml = StringIO.StringIO(s) - tree = xml_parser.parse(pack_xml) - pack = Pack(ns, name) - pack.load(tree.getroot()) - return pack + def pack_from_str(self, ns, name, s): + pack_xml = StringIO.StringIO(s) + tree = xml_parser.parse(pack_xml) + pack = Pack(ns, name) + pack.load(tree.getroot()) + return pack - def setUp(self): - self.ns = 'old.name.space' - self.p = Pack(self.ns, 'my_pack') + def setUp(self): + self.ns = 'old.name.space' + self.p = Pack(self.ns, 'my_pack') - def tearDown(self): - pass + def tearDown(self): + pass class AliasTest(PackTest): - def test_alias_empty(self): - new_ns = 'new.name.space' - self.assertNotEqual(self.p.namespace, new_ns) - self.p.alias(new_ns) - self.assertEqual(self.p.namespace, new_ns) - - def test_alias_requires_matches(self): - required = '{}.foo1'.format(self.ns) - new_ns = 'new.name.space' - expected = ['{}.foo1'.format(new_ns)] - self.p.requires.append(required) - self.assertEqual(self.p.requires, [required]) - self.p.alias(new_ns) - self.assertEqual(self.p.requires, expected) + def test_alias_empty(self): + new_ns = 'new.name.space' + self.assertNotEqual(self.p.namespace, new_ns) + self.p.alias(new_ns) + self.assertEqual(self.p.namespace, new_ns) + + def test_alias_requires_matches(self): + required = '{}.foo1'.format(self.ns) + new_ns = 'new.name.space' + expected = ['{}.foo1'.format(new_ns)] + self.p.requires.append(required) + self.assertEqual(self.p.requires, [required]) + self.p.alias(new_ns) + self.assertEqual(self.p.requires, expected) TEST_DEFAULT_COPY_XML = """ @@ -84,217 +84,222 @@ TEST_DEFAULT_COPY_XML = """ """ class AttributeExistenceTest(PackTest): - def test_pack_valid_full(self): - xml = '<pack name="foo"><defaults/></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_pack_valid_named_null(self): - xml = '<pack name="foo"/>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_pack_invalid_no_name(self): - xml = '<pack/>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_pack_invalid_extra_attr(self): - xml = '<pack name="blah" secret="x12"/>' - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_defaults_valid_no_attrs(self): - xml = '<pack name="test"><defaults/></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_defaults_invalid_extra(self): - xml = '<pack name="test"><defaults something="is_here"/></pack>' - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_defaults_copy_valid_no_attrs(self): - xml = '<pack name="test"><defaults><copy/></defaults></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_defaults_copy_valid_all_attrs(self): - xml = '<pack name="test"><defaults><copy to="/me/"/></defaults></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_defaults_copy_invalid_extra(self): - xml = ('<pack name="test"><defaults><copy to="/me/" from="/baz"/>' - '</defaults></pack>') - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_defaults_copy_acl_valid_no_attrs(self): - xml = ('<pack name="test"><defaults><copy>' - '<set-acl/></copy></defaults></pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_defaults_copy_acl_valid_all_attrs(self): - xml = ('<pack name="test"><defaults><copy>' - '<set-acl perms="0600" fcaps="" ' - 'user="a_user" group="a_group" ' - 'selabel="test_t"/></copy></defaults></pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_defaults_copy_acl_invalid_extra(self): - xml = ('<pack name="test"><defaults><copy>' - '<set-acl perms="0600" fcaps="CAP_SYS_ADMIN CAP_CHOWN" ' - 'user="a_user" group="a_group" ' - 'selabel="test_t" apparmor_p="gfoo"/></copy></defaults></pack>') - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_copy_invalid_no_attrs(self): - xml = '<pack name="test"><copy/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_copy_invalid_extra(self): - xml = '<pack name="test"><copy to="/hi/" from="/bar" baz="123"/></pack>' - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_copy_valid_minimal(self): - xml = '<pack name="test"><copy to="/x" from="/y"/></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_copy_valid_all(self): - xml = '<pack name="test"><copy to="/x/" from="/y/*" recurse="true"/></pack>' - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_config_invalid_no_attrs(self): - xml = '<pack name="test"><config/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_config_invalid_path_only(self): - xml = '<pack name="test"><config path="/foo"/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_config_invalid_type_only(self): - xml = '<pack name="test"><config type="sepolicy"/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_config_invalid_extra(self): - xml = ('<pack name="test">' - '<config type="kernel-fragment" path="/foo" z="1"/></pack>') - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_config_valid_all(self): - xml = ('<pack name="test"><config path="/foo" type="kernel-fragment"/>' - '</pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_provides_invalid_bare(self): - xml = '<pack name="test"><provides/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_provides_invalid_extra(self): - xml = ('<pack name="test">' - '<provides pack="hello.bar" from="somewhere"/></pack>') - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_provides_valid(self): - xml = ('<pack name="test">' - '<provides pack="hello.bar"/></pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_requires_invalid_bare(self): - xml = '<pack name="test"><requires/></pack>' - with self.assertRaises(common.MissingAttribute): - self.pack_from_str('a', 'b', xml) - - def test_requires_invalid_extra(self): - xml = ('<pack name="test">' - '<requires pack="hello.bar" from="somewhere"/></pack>') - with self.assertRaises(common.UnknownAttributes): - self.pack_from_str('a', 'b', xml) - - def test_requires_valid(self): - xml = ('<pack name="test">' - '<requires pack="hello.bar"/></pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) + def test_pack_valid_full(self): + xml = '<pack name="foo"><defaults/></pack>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_pack_valid_named_null(self): + xml = '<pack name="foo"/>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_pack_invalid_no_name(self): + xml = '<pack/>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_pack_invalid_extra_attr(self): + xml = '<pack name="blah" secret="x12"/>' + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_defaults_valid_no_attrs(self): + xml = '<pack name="test"><defaults/></pack>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_defaults_invalid_extra(self): + xml = '<pack name="test"><defaults something="is_here"/></pack>' + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_defaults_copy_valid_no_attrs(self): + xml = '<pack name="test"><defaults><copy/></defaults></pack>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_defaults_copy_valid_all_attrs(self): + xml = '<pack name="test"><defaults><copy to="/me/"/></defaults></pack>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_defaults_copy_invalid_extra(self): + xml = ('<pack name="test"><defaults><copy to="/me/" from="/baz"/>' + '</defaults></pack>') + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_defaults_copy_acl_valid_no_attrs(self): + xml = ('<pack name="test"><defaults><copy>' + '<set-acl/></copy></defaults></pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_defaults_copy_acl_valid_all_attrs(self): + xml = ('<pack name="test"><defaults><copy>' + '<set-acl perms="0600" fcaps="" ' + 'user="a_user" group="a_group" ' + 'selabel="test_t"/></copy></defaults></pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_defaults_copy_acl_invalid_extra(self): + xml = ('<pack name="test"><defaults><copy>' + '<set-acl perms="0600" fcaps="CAP_SYS_ADMIN CAP_CHOWN" ' + 'user="a_user" group="a_group" ' + 'selabel="test_t" apparmor_p="gfoo"/></copy></defaults></pack>') + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_copy_invalid_no_attrs(self): + xml = '<pack name="test"><copy/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_copy_invalid_extra(self): + xml = '<pack name="test"><copy to="/hi/" from="/bar" baz="123"/></pack>' + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_copy_valid_minimal(self): + xml = '<pack name="test"><copy to="/x" from="/y"/></pack>' + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_copy_valid_all(self): + xml = ('<pack name="test"><copy to="/x/" from="/y/*" recurse="true"/>' + '</pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_config_invalid_no_attrs(self): + xml = '<pack name="test"><config/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_config_invalid_path_only(self): + xml = '<pack name="test"><config path="/foo"/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_config_invalid_type_only(self): + xml = '<pack name="test"><config type="sepolicy"/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_config_invalid_extra(self): + xml = ('<pack name="test">' + '<config type="kernel-fragment" path="/foo" z="1"/></pack>') + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_config_valid_all(self): + xml = ('<pack name="test"><config path="/foo" type="kernel-fragment"/>' + '</pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_provides_invalid_bare(self): + xml = '<pack name="test"><provides/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_provides_invalid_extra(self): + xml = ('<pack name="test">' + '<provides pack="hello.bar" from="somewhere"/></pack>') + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_provides_valid(self): + xml = ('<pack name="test">' + '<provides pack="hello.bar"/></pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_requires_invalid_bare(self): + xml = '<pack name="test"><requires/></pack>' + with self.assertRaises(common.MissingAttribute): + self.pack_from_str('a', 'b', xml) + + def test_requires_invalid_extra(self): + xml = ('<pack name="test">' + '<requires pack="hello.bar" from="somewhere"/></pack>') + with self.assertRaises(common.UnknownAttributes): + self.pack_from_str('a', 'b', xml) + + def test_requires_valid(self): + xml = ('<pack name="test">' + '<requires pack="hello.bar"/></pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) class ConfigTest(PackTest): - # TODO(wad) Replace all '/' with os.path.sep. - def test_valid_types(self): - for t in ('sepolicy', 'kernel-fragment'): - xml = '<pack name="test"><config path="/foo" type="{}"/></pack>'.format(t) - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - - def test_invalid_types(self): - for t in ('a_type', 'firmware-fragment'): - xml = '<pack name="test"><config path="/foo" type="{}"/></pack>'.format(t) - with self.assertRaises(common.LoadErrorWithOrigin): - self.pack_from_str('a', 'b', xml) - - def test_trailing_slash(self): - xml = ('<pack name="test"><config path="/foo/" type="kernel-fragment"/>' - '</pack>') - with self.assertRaises(common.LoadErrorWithOrigin): - self.pack_from_str('a', 'b', xml) - - def test_absolute_path(self): - xml = ('<pack name="test"><config path="/foo" type="kernel-fragment"/>' - '</pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - self.assertEqual(pack.configs[0].path, '/foo') - - def test_relative_path(self): - xml = ('<pack name="test"><config path="foo.kconfig" ' - 'type="kernel-fragment"/></pack>') - pack = self.pack_from_str('a', 'b', xml) - self.assertIsInstance(pack, Pack) - self.assertEqual(pack.configs[0].path, - os.path.join(os.getcwd(), 'foo.kconfig')) + # TODO(wad) Replace all '/' with os.path.sep. + def test_valid_types(self): + for t in ('sepolicy', 'kernel-fragment'): + xml = ('<pack name="test"><config path="/foo" type="{}"/>' + '</pack>'.format(t)) + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + + def test_invalid_types(self): + for t in ('a_type', 'firmware-fragment'): + xml = ('<pack name="test"><config path="/foo" type="{}"/>' + '</pack>'.format(t)) + with self.assertRaises(common.LoadErrorWithOrigin): + self.pack_from_str('a', 'b', xml) + + def test_trailing_slash(self): + xml = ('<pack name="test"><config path="/foo/" type="kernel-fragment"/>' + '</pack>') + with self.assertRaises(common.LoadErrorWithOrigin): + self.pack_from_str('a', 'b', xml) + + def test_absolute_path(self): + xml = ('<pack name="test"><config path="/foo" type="kernel-fragment"/>' + '</pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + self.assertEqual(pack.configs[0].path, '/foo') + + def test_relative_path(self): + xml = ('<pack name="test"><config path="foo.kconfig" ' + 'type="kernel-fragment"/></pack>') + pack = self.pack_from_str('a', 'b', xml) + self.assertIsInstance(pack, Pack) + self.assertEqual(pack.configs[0].path, + os.path.join(os.getcwd(), 'foo.kconfig')) class DefaultCopyPropagationTest(PackTest): - def setUp(self): - self._pack = self.pack_from_str('a_namespace', 'a_pack', - TEST_DEFAULT_COPY_XML) - - def test_default_user_merge(self): - for node in self._pack.copies: - if node.src == '/d/place': - self.assertEqual(node.dst, '/a/location/place') - self.assertEqual(node.acl.group, 'merge') - self.assertEqual(node.acl.user, 'some_user') - - def test_default_noclobber(self): - for node in self._pack.copies: - if node.src == '/b/place': - self.assertEqual(node.dst, '/c/place') - self.assertEqual(node.acl.user, 'dont_overwrite_me') - self.assertEqual(node.acl.group, 'a_group') - - def test_default_to_empty(self): - for node in self._pack.copies: - if node.src == '/a/place': - self.assertEqual(node.dst, '/a/location/place') - self.assertEqual(node.acl.user, 'some_user') - self.assertEqual(node.acl.group, node.acl.DEFAULTS['GROUP']) - self.assertEqual(node.acl.selabel, node.acl.DEFAULTS['SELABEL']) - self.assertEqual(node.acl.fcaps, node.acl.DEFAULTS['CAPABILITIES']) - self.assertEqual(node.acl.perms, node.acl.DEFAULTS['PERMISSIONS']) + def setUp(self): + self._pack = self.pack_from_str('a_namespace', 'a_pack', + TEST_DEFAULT_COPY_XML) + + def test_default_user_merge(self): + for node in self._pack.copies: + if node.src == '/d/place': + self.assertEqual(node.dst, '/a/location/place') + self.assertEqual(node.acl.group, 'merge') + self.assertEqual(node.acl.user, 'some_user') + + def test_default_noclobber(self): + for node in self._pack.copies: + if node.src == '/b/place': + self.assertEqual(node.dst, '/c/place') + self.assertEqual(node.acl.user, 'dont_overwrite_me') + self.assertEqual(node.acl.group, 'a_group') + + def test_default_to_empty(self): + for node in self._pack.copies: + if node.src == '/a/place': + self.assertEqual(node.dst, '/a/location/place') + self.assertEqual(node.acl.user, 'some_user') + self.assertEqual(node.acl.group, node.acl.DEFAULTS['GROUP']) + self.assertEqual(node.acl.selabel, node.acl.DEFAULTS['SELABEL']) + self.assertEqual(node.acl.fcaps, + node.acl.DEFAULTS['CAPABILITIES']) + self.assertEqual(node.acl.perms, + node.acl.DEFAULTS['PERMISSIONS']) diff --git a/cli/lib/project/packmap.py b/cli/lib/project/packmap.py index 07284d0..3315b66 100644 --- a/cli/lib/project/packmap.py +++ b/cli/lib/project/packmap.py @@ -26,184 +26,187 @@ from project import packs class UpdateError(common.LoadErrorWithOrigin): - pass + pass class PackMap(object): - """PackMap provides a map of unique pack names to pack objects. - - Additionally, PackMap acts as the centralized location for any - other indexing needed by its consumers to minimize inconsistency. - """ - def __init__(self): - # { pack.uid => pack } - self._map = {} - # { virtual_uid => [providing pack list] - self._provides = defaultdict(list) - # {source_files => [pack_uids] } - self._origins = defaultdict(list) - # [ required_uid => [requiring pack list] ] - self._missing = defaultdict(list) - # Imported Packs objects - self._packs = [] - # { path => [Copy()] } - self._destinations = defaultdict(list) - - @property - def map(self): - return self._map - - @property - def copy_destinations(self): - return self._destinations - - @property - def packs(self): - return self._packs - - @property - def virtuals(self): - return self._provides - - @property - def origins(self): - return self._origins - - @property - def missing(self): - return self._missing - - def submap(self, pack_uid, aliases): - """Returns a PackMap containing just the packs in a tree from @pack_uid. - - Raises: - dependency.Error: if there are any unfulfilled dependencies. - """ - pm = PackMap() - p = [self.map[pack_uid]] - pm.update(p) - while len(p) != 0: - # Iterate until we can no longer resolve pm.missing() from map - p = [] - for uid in pm.missing: - if uid in self.map: - p.append(self.map[uid]) - # Automatically pick an implementation if there is - # only one that matches a prefix above. - if uid in self.virtuals: - provides = [] - for alias, prefix in aliases.iteritems(): - if uid.startswith('{}.'.format(alias)): - provides += [x for x in self.virtuals[uid] - if x.startswith(prefix)] - if len(provides) == 1: - p.append(self.map[provides[0]]) - pm.update(p) - - unsatisfied = [] - for missing, req_uids in pm.missing.iteritems(): - if missing in self.virtuals: - unsatisfied.append(dependency.Virtual( - missing, pack_uid, req_uids, self.virtuals[missing])) - if len(unsatisfied): - raise dependency.UnsatisfiedVirtualPackError(unsatisfied) - if len(pm.missing): - pm.report_missing() - return pm - - def report_missing(self): - """Raise a UndefinedPackError with useful text for all missing - required packages. - """ - undef = [] - for req_uid, needed_by in self.missing.iteritems(): - undef.append( - dependency.Undefined(req_uid, [self.map[u] for u in needed_by])) - if len(undef): - raise dependency.UndefinedPackError(undef) - - def update(self, packs_): - """Merges a Packs object (or container of Pack objects) into the PackMap""" - self._packs += [packs_] - for pack in packs_: - if pack.uid in self._map: - # Check if the caller passed in an already seen pack by checking - # the origins. - if pack.origin == self._map[pack.uid].origin: - continue - raise UpdateError( - pack.origin, - 'Redefinition of pack "{}". Previously defined here: {}'.format( - pack.uid, self._map[pack.uid].origin)) - if pack.uid in self._provides: - prevs = [self._map[p] for p in self._provides[pack.uid]] - msg = 'Redefinition of virtual pack "{}". '.format(pack.uid) - msg += 'Previously declared as provided by ' - msgs = [] - for p in prevs: - msgs += ['pack "{}" defined at "{}"'.format(p.uid, p.origin)] - msg += '{}.'.format(', '.join(msgs)) - raise UpdateError(pack.origin, msg) - self._map[pack.uid] = pack - for provides in pack.provides: - if provides in self._map: - raise UpdateError( - pack.origin, - 'Pack "{}" provides declaration conflicts with pack ' - '"{}" previously defined here: {}'.format( - pack.uid, provides, self._map[provides].origin)) - self._provides[provides] += [pack.uid] - # Check if the provides fills any open dependencies. - if provides in self._missing: - del self._missing[provides] - - self._origins[pack.origin.source_file] += [pack.uid] - # Create a quick reference for which packs claim which files. - # The conflict packs themselves can be found via copy.pack - for copy in pack.copies: - self._destinations[copy.dst] += [copy] - - # Check if this pack fulfills any open dependencies. - if pack.uid in self._missing: - del self._missing[pack.uid] - - # Check if this pack has any open dependencies. - for requires in pack.requires: - if (requires not in self._map and - requires not in self._provides): - self._missing[requires].append(pack.uid) - - def check_paths(self): - """Checks the packmap for destination conflicts. - - As wildcards and recursion leave ambiguity, this is - best effort to follow the fail-early user experience. + """PackMap provides a map of unique pack names to pack objects. + + Additionally, PackMap acts as the centralized location for any + other indexing needed by its consumers to minimize inconsistency. """ - for dst, copies in self.copy_destinations.iteritems(): - if len(copies) > 1: - raise common.PathConflictError( - copies[0].origin, - 'Multiple sources for one destination "{}": {}'.format( - dst, [c.pack.uid for c in copies])) + def __init__(self): + # { pack.uid => pack } + self._map = {} + # { virtual_uid => [providing pack list] + self._provides = defaultdict(list) + # {source_files => [pack_uids] } + self._origins = defaultdict(list) + # [ required_uid => [requiring pack list] ] + self._missing = defaultdict(list) + # Imported Packs objects + self._packs = [] + # { path => [Copy()] } + self._destinations = defaultdict(list) + + @property + def map(self): + return self._map + + @property + def copy_destinations(self): + return self._destinations + + @property + def packs(self): + return self._packs + + @property + def virtuals(self): + return self._provides + + @property + def origins(self): + return self._origins + + @property + def missing(self): + return self._missing + + def submap(self, pack_uid, aliases): + """Returns a PackMap containing just the packs in a tree from @pack_uid. + + Raises: + dependency.Error: if there are any unfulfilled dependencies. + """ + pm = PackMap() + p = [self.map[pack_uid]] + pm.update(p) + while len(p) != 0: + # Iterate until we can no longer resolve pm.missing() from map + p = [] + for uid in pm.missing: + if uid in self.map: + p.append(self.map[uid]) + # Automatically pick an implementation if there is + # only one that matches a prefix above. + if uid in self.virtuals: + provides = [] + for alias, prefix in aliases.iteritems(): + if uid.startswith('{}.'.format(alias)): + provides += [x for x in self.virtuals[uid] + if x.startswith(prefix)] + if len(provides) == 1: + p.append(self.map[provides[0]]) + pm.update(p) + + unsatisfied = [] + for missing, req_uids in pm.missing.iteritems(): + if missing in self.virtuals: + unsatisfied.append(dependency.Virtual( + missing, pack_uid, req_uids, self.virtuals[missing])) + if len(unsatisfied): + raise dependency.UnsatisfiedVirtualPackError(unsatisfied) + if len(pm.missing): + pm.report_missing() + return pm + + def report_missing(self): + """Raise a UndefinedPackError with useful text for all missing + required packages. + """ + undef = [] + for req_uid, needed_by in self.missing.iteritems(): + undef.append( + dependency.Undefined(req_uid, [self.map[u] for u in needed_by])) + if len(undef): + raise dependency.UndefinedPackError(undef) + + def update(self, packs_): + """Merges a Packs object (or container of Pack objects) into the + PackMap + """ + self._packs += [packs_] + for pack in packs_: + if pack.uid in self._map: + # Check if the caller passed in an already seen pack by checking + # the origins. + if pack.origin == self._map[pack.uid].origin: + continue + raise UpdateError( + pack.origin, + ('Redefinition of pack "{}". Previously defined here: ' + '{}'.format(pack.uid, self._map[pack.uid].origin))) + if pack.uid in self._provides: + prevs = [self._map[p] for p in self._provides[pack.uid]] + msg = 'Redefinition of virtual pack "{}". '.format(pack.uid) + msg += 'Previously declared as provided by ' + msgs = [] + for p in prevs: + msgs += ['pack "{}" defined at "{}"'.format(p.uid, + p.origin)] + msg += '{}.'.format(', '.join(msgs)) + raise UpdateError(pack.origin, msg) + self._map[pack.uid] = pack + for provides in pack.provides: + if provides in self._map: + raise UpdateError( + pack.origin, + 'Pack "{}" provides declaration conflicts with pack ' + '"{}" previously defined here: {}'.format( + pack.uid, provides, self._map[provides].origin)) + self._provides[provides] += [pack.uid] + # Check if the provides fills any open dependencies. + if provides in self._missing: + del self._missing[provides] + + self._origins[pack.origin.source_file] += [pack.uid] + # Create a quick reference for which packs claim which files. + # The conflict packs themselves can be found via copy.pack + for copy in pack.copies: + self._destinations[copy.dst] += [copy] + + # Check if this pack fulfills any open dependencies. + if pack.uid in self._missing: + del self._missing[pack.uid] + + # Check if this pack has any open dependencies. + for requires in pack.requires: + if (requires not in self._map + and requires not in self._provides): + self._missing[requires].append(pack.uid) + + def check_paths(self): + """Checks the packmap for destination conflicts. + + As wildcards and recursion leave ambiguity, this is + best effort to follow the fail-early user experience. + """ + for dst, copies in self.copy_destinations.iteritems(): + if len(copies) > 1: + raise common.PathConflictError( + copies[0].origin, + 'Multiple sources for one destination "{}": {}'.format( + dst, [c.pack.uid for c in copies])) if __name__ == "__main__": - # Example usage to remain until captured in unittests. - import sys - packmap = PackMap() - re_ns = 0 - for files in sys.argv[1:]: - my_packs = packs.PacksFactory().new(path=os.path.abspath(files)) - print 'Loaded packs from: {}'.format(os.path.abspath(files)) - if re_ns: - my_packs.namespace = 'example.alias.ns' - packmap.update(my_packs) - re_ns = (re_ns + 1) % 2 - print 'Global packs: {}'.format(packmap.map.keys()) - print 'Global virtual packs: {}'.format(packmap.virtuals.keys()) - print 'Missing requirements: {}'.format(packmap.missing) - spm = packmap.submap(packmap.map.keys()[1], '') - print 'Submap for {}:'.format(packmap.map.keys()[1]) - print '--] map: {}'.format(spm.map.keys()) - print '--] virtuals: {}'.format(spm.virtuals.keys()) - print '--] missing: {}'.format(spm.missing.keys()) + # Example usage to remain until captured in unittests. + import sys + packmap = PackMap() + re_ns = 0 + for files in sys.argv[1:]: + my_packs = packs.PacksFactory().new(path=os.path.abspath(files)) + print 'Loaded packs from: {}'.format(os.path.abspath(files)) + if re_ns: + my_packs.namespace = 'example.alias.ns' + packmap.update(my_packs) + re_ns = (re_ns + 1) % 2 + print 'Global packs: {}'.format(packmap.map.keys()) + print 'Global virtual packs: {}'.format(packmap.virtuals.keys()) + print 'Missing requirements: {}'.format(packmap.missing) + spm = packmap.submap(packmap.map.keys()[1], '') + print 'Submap for {}:'.format(packmap.map.keys()[1]) + print '--] map: {}'.format(spm.map.keys()) + print '--] virtuals: {}'.format(spm.virtuals.keys()) + print '--] missing: {}'.format(spm.missing.keys()) diff --git a/cli/lib/project/packmap_stub.py b/cli/lib/project/packmap_stub.py index 5bbfd57..4edef17 100644 --- a/cli/lib/project/packmap_stub.py +++ b/cli/lib/project/packmap_stub.py @@ -20,11 +20,11 @@ class StubPackMap(object): - def __init__(self, pack_map=None, provides=None, missing=None, origins=None, - packs=None, destinations=None): - self.map = pack_map or {} - self.provides = provides or {} - self.missing = missing or {} - self.origins = origins or {} - self.copy_destinations = destinations or {} - self.packs = packs or [] + def __init__(self, pack_map=None, provides=None, missing=None, origins=None, + packs=None, destinations=None): + self.map = pack_map or {} + self.provides = provides or {} + self.missing = missing or {} + self.origins = origins or {} + self.copy_destinations = destinations or {} + self.packs = packs or [] diff --git a/cli/lib/project/packs.py b/cli/lib/project/packs.py index 0d54c26..8927ac7 100644 --- a/cli/lib/project/packs.py +++ b/cli/lib/project/packs.py @@ -24,90 +24,91 @@ from project import pack class PacksFactory(object): - @staticmethod - def new(**kwargs): - """Creates a new Packs instance from an XML file or element. - - Walks an ElementTree element looking for <pack> child - nodes and instantiates Pack objects. - - Args: - **kwargs: Any valid keywords for loader.PathElementLoader.load() - - Returns: - new Packs() instance - - Raises - LoadError: LoadError, or a subclass, will be raised if an error occurs - loading and validating content from the XML. - """ - ps = Packs() - l = loader.PathElementLoader('packs', ['namespace']) - # Get the final root node. - root = l.load(**kwargs) - root.limit_attribs(['version', 'namespace', 'path']) - ps.namespace = root.get_attrib('namespace') - ns = ps.namespace - # pylint: disable=no-member - if 'namespace' in root.old_attrib: - ns = root.old_attrib['namespace'] - - packs = {} - for node in root.findall('pack'): - name = node.get_attrib('name') - if name in packs: - # Note, this only catch duplication within the same - # packs tree. As packs namespaces are non-unique, collision - # can occur when aggregating packs. - raise common.LoadErrorWithOrigin( - node, - 'Duplicate pack {} in namespace {}'.format(name, ps.namespace)) - p = pack.Pack(ns, name) - p.load(node) - # Even if 'namespace' is carried over in the root attributes, we have to - # alias any global requires/provides in the pack. - if ns != ps.namespace: - p.alias(ps.namespace) - packs[name] = p - - ps.entries = packs - ps.origins = l.origins - return ps + @staticmethod + def new(**kwargs): + """Creates a new Packs instance from an XML file or element. + + Walks an ElementTree element looking for <pack> child + nodes and instantiates Pack objects. + + Args: + **kwargs: Any valid keywords for loader.PathElementLoader.load() + + Returns: + new Packs() instance + + Raises + LoadError: LoadError, or a subclass, will be raised if an error + occurs loading and validating content from the XML. + """ + ps = Packs() + l = loader.PathElementLoader('packs', ['namespace']) + # Get the final root node. + root = l.load(**kwargs) + root.limit_attribs(['version', 'namespace', 'path']) + ps.namespace = root.get_attrib('namespace') + ns = ps.namespace + # pylint: disable=no-member + if 'namespace' in root.old_attrib: + ns = root.old_attrib['namespace'] + + packs = {} + for node in root.findall('pack'): + name = node.get_attrib('name') + if name in packs: + # Note, this only catch duplication within the same + # packs tree. As packs namespaces are non-unique, collision + # can occur when aggregating packs. + raise common.LoadErrorWithOrigin( + node, + 'Duplicate pack {} in namespace {}'.format(name, + ps.namespace)) + p = pack.Pack(ns, name) + p.load(node) + # Even if 'namespace' is carried over in the root attributes, we + # have to alias any global requires/provides in the pack. + if ns != ps.namespace: + p.alias(ps.namespace) + packs[name] = p + + ps.entries = packs + ps.origins = l.origins + return ps class Packs(collection.Base): - """Collection of Pack objects.""" - def __init__(self): - super(Packs, self).__init__('packs') - self._namespace = () - - @property - def packs(self): - return self.entries - - @property - def namespace(self): - """Return the namespace for all child pack objects.""" - return self._namespace - - @namespace.setter - def namespace(self, ns): - for pack_ in self._entries.values(): - pack_.alias(ns) - self._namespace = ns - - def add_pack(self, new_pack): - if new_pack.name in self.entries: - # Note, this only catch duplication within the same - # packs tree. As packs namespaces are non-unique, collision - # can occur when aggregating packs. - raise common.LoadErrorWithOrigin( - new_pack.origin, - 'Duplicate pack {} in namespace {}'.format(new_pack.name, - self.namespace)) - self.entries[new_pack.name] = new_pack - - - def __repr__(self): - return '<packs namespace="{}">{}</packs>'.format( - self._namespace, ''.join([str(p) for p in self._entries.values()])) + """Collection of Pack objects.""" + def __init__(self): + super(Packs, self).__init__('packs') + self._namespace = () + + @property + def packs(self): + return self.entries + + @property + def namespace(self): + """Return the namespace for all child pack objects.""" + return self._namespace + + @namespace.setter + def namespace(self, ns): + for pack_ in self._entries.values(): + pack_.alias(ns) + self._namespace = ns + + def add_pack(self, new_pack): + if new_pack.name in self.entries: + # Note, this only catch duplication within the same + # packs tree. As packs namespaces are non-unique, collision + # can occur when aggregating packs. + raise common.LoadErrorWithOrigin( + new_pack.origin, + 'Duplicate pack {} in namespace {}'.format(new_pack.name, + self.namespace)) + self.entries[new_pack.name] = new_pack + + + def __repr__(self): + return '<packs namespace="{}">{}</packs>'.format( + self._namespace, ''.join([str(p) for p in self._entries.values()])) diff --git a/cli/lib/project/project_spec.py b/cli/lib/project/project_spec.py index 9f89f64..21a2f2e 100644 --- a/cli/lib/project/project_spec.py +++ b/cli/lib/project/project_spec.py @@ -15,14 +15,14 @@ """ - This file is the parsing entry point for BDK XML project. + This file is the parsing entry point for BDK XML project. - BDK XML configuration is specified in the schemas/ path and - all input files should be verifiable with a RELAX-NG verifier - like 'jing'. + BDK XML configuration is specified in the schemas/ path and + all input files should be verifiable with a RELAX-NG verifier + like 'jing'. - At present, this entry point only performs content validation - and not formal syntax validation. + At present, this entry point only performs content validation + and not formal syntax validation. """ @@ -37,169 +37,172 @@ from project import xml_parser class ProjectSpec(object): - """ProjectSpec represents the primary interface to a user-defined project. - - The project is created from a BDK project XML file (defined by - the bdk.rng RELAX NG schema). - """ - def __init__(self): - self._version = 1 - self._targets = {} - self._config = config.Config() - self._packmap = packmap.PackMap() - self._origin = common.Origin() - - @property - def config(self): - """Returns the Config object.""" - return self._config - - @config.setter - def config(self, config_): - self._config = config_ - - @property - def packmap(self): - """Returns the PackMap object.""" - return self._packmap - - def add_target(self, target): - if target.name in self._targets: - raise common.LoadErrorWithOrigin( - target.origin, - 'Target "{}" redefined. Previously definition here: "{}"'.format( - target.name, self._targets[target.name].origin)) - self._targets[target.name] = target - - def add_packs(self, packs_obj): - self._packmap.update(packs_obj) - - @property - def targets(self): - """Returns a dict of Targets keyed by name.""" - return self._targets - - @property - def origin(self): - return self._origin - - @origin.setter - def origin(self, o): - self._origin = o.copy() - - @property - def version(self): - return self._version - - @version.setter - def version(self, v): - self._version = v - - def __repr__(self): - return ('<project version="{}" origins="{}">' - '{}{}{}</project>').format( - self._version, self.packmap.origins, self._config, - self.packmap.packs, self.targets) - - @classmethod - def from_xml(cls, src='project.xml'): - """Populates this instance with the ProjectSpec from file. - - To validate the resulting ProjectSpec, - config.packmap.report_missing() - may be called to identify globally undefined packs. - - And config.targets[t].create_submap(config.packmap) - must be called to identify unsatisfied dependencies or undefined - pack names per-target. - - Args: - src: If str, a path to XML file defining a <project>. - If file compatible, the XML file. - - Returns: - instance of ProjectSpec - - Raises: - IOError - common.LoadError or ones of its subclasses: - common.LoadErrorWithOrigin - common.PathConflictError - packmap.UpdateError - """ - project_spec = cls() - if isinstance(src, basestring): - src = os.path.abspath(src) - tree = xml_parser.parse(src) - root = tree.getroot() - # pylint: disable=no-member - project_spec.origin = root.origin - root.limit_attribs(['version']) - # Ensure we have a project document. - if root.tag != 'project': - # pylint: disable=no-member - raise common.LoadErrorWithOrigin(root.origin, - 'root node not <project>') - - if 'version' in root.attrib: - project_spec.version = root.attrib['version'] - - # Pull in each top level element. - cfg = root.findall('config') - if len(cfg) > 1: - raise common.LoadErrorWithOrigin( - cfg[1].origin, 'Only one <config> element may be defined.') - if len(cfg): - project_spec.config = config.Config.from_element(cfg[0]) - cls.collect_packs(root, project_spec.add_packs) - cls.collect_targets(root, project_spec.packmap, project_spec.add_target) - if (project_spec.config.default_target and - project_spec.config.default_target not in project_spec.targets): - # TODO(wad) The origin could be made more specific we keep the origins - # per-child around. - raise common.LoadErrorWithOrigin( - project_spec.config.origin, - 'default target "{}" is not defined.'.format( - project_spec.config.default_target)) - # If there's only one target and no default, set the default to that one. - if (not project_spec.config.default_target and - len(project_spec.targets)) == 1: - project_spec.config.default_target = project_spec.targets.keys()[0] - return project_spec - - @staticmethod - def collect_packs(root_node, callback): - """Collects all <Packs> passing them to callback(packs).""" - for node in root_node.findall('packs'): - callback(packs.PacksFactory().new(element=node)) - - @staticmethod - def collect_targets(root, packmap_, callback): - """Collects all <target> from <targets> passing them to callback(target).""" - for node in root.findall('targets'): - t = targets.TargetsFactory.new(packmap_, element=node) - for tgt in t.targets.values(): - callback(tgt) - - def get_target(self, target_name=None): - """Gets a target by name, or the default target if none is specified. - - Raises: - KeyError: if the spec does not have the requested target, - or the default target is requested but not defined. + """ProjectSpec represents the primary interface to a user-defined project. + + The project is created from a BDK project XML file (defined by + the bdk.rng RELAX NG schema). """ - # Try to find a default target if not specified. - if not target_name: - if self.config.default_target: - target_name = self.config.default_target - else: - raise KeyError('{}: No default target could be found ' - 'in project spec. Possible targets are: {}.'.format( - self.origin, self.targets.keys())) - - # Check that the desired target exists. - if target_name not in self.targets: - raise KeyError('{}: No such target "{}" (options are: {})'.format( - self.origin, target_name, self.targets.keys())) - - return self.targets[target_name] + def __init__(self): + self._version = 1 + self._targets = {} + self._config = config.Config() + self._packmap = packmap.PackMap() + self._origin = common.Origin() + + @property + def config(self): + """Returns the Config object.""" + return self._config + + @config.setter + def config(self, config_): + self._config = config_ + + @property + def packmap(self): + """Returns the PackMap object.""" + return self._packmap + + def add_target(self, target): + if target.name in self._targets: + raise common.LoadErrorWithOrigin( + target.origin, + ('Target "{}" redefined. Previously definition here: ' + '"{}"'.format(target.name, self._targets[target.name].origin))) + self._targets[target.name] = target + + def add_packs(self, packs_obj): + self._packmap.update(packs_obj) + + @property + def targets(self): + """Returns a dict of Targets keyed by name.""" + return self._targets + + @property + def origin(self): + return self._origin + + @origin.setter + def origin(self, o): + self._origin = o.copy() + + @property + def version(self): + return self._version + + @version.setter + def version(self, v): + self._version = v + + def __repr__(self): + return ('<project version="{}" origins="{}">' + '{}{}{}</project>').format( + self._version, self.packmap.origins, self._config, + self.packmap.packs, self.targets) + + @classmethod + def from_xml(cls, src='project.xml'): + """Populates this instance with the ProjectSpec from file. + + To validate the resulting ProjectSpec, + config.packmap.report_missing() + may be called to identify globally undefined packs. + + And config.targets[t].create_submap(config.packmap) + must be called to identify unsatisfied dependencies or undefined + pack names per-target. + + Args: + src: If str, a path to XML file defining a <project>. + If file compatible, the XML file. + + Returns: + instance of ProjectSpec + + Raises: + IOError + common.LoadError or ones of its subclasses: + common.LoadErrorWithOrigin + common.PathConflictError + packmap.UpdateError + """ + project_spec = cls() + if isinstance(src, basestring): + src = os.path.abspath(src) + tree = xml_parser.parse(src) + root = tree.getroot() + # pylint: disable=no-member + project_spec.origin = root.origin + root.limit_attribs(['version']) + # Ensure we have a project document. + if root.tag != 'project': + # pylint: disable=no-member + raise common.LoadErrorWithOrigin(root.origin, + 'root node not <project>') + + if 'version' in root.attrib: + project_spec.version = root.attrib['version'] + + # Pull in each top level element. + cfg = root.findall('config') + if len(cfg) > 1: + raise common.LoadErrorWithOrigin( + cfg[1].origin, 'Only one <config> element may be defined.') + if len(cfg): + project_spec.config = config.Config.from_element(cfg[0]) + cls.collect_packs(root, project_spec.add_packs) + cls.collect_targets(root, project_spec.packmap, project_spec.add_target) + if (project_spec.config.default_target and + project_spec.config.default_target not in project_spec.targets): + # TODO(wad) The origin could be made more specific we keep the + # origins per-child around. + raise common.LoadErrorWithOrigin( + project_spec.config.origin, + 'default target "{}" is not defined.'.format( + project_spec.config.default_target)) + # If there's only one target and no default, set the default to that + # one. + if (not project_spec.config.default_target and + len(project_spec.targets)) == 1: + project_spec.config.default_target = project_spec.targets.keys()[0] + return project_spec + + @staticmethod + def collect_packs(root_node, callback): + """Collects all <Packs> passing them to callback(packs).""" + for node in root_node.findall('packs'): + callback(packs.PacksFactory().new(element=node)) + + @staticmethod + def collect_targets(root, packmap_, callback): + """Collects all <target> from <targets> passing them to + callback(target). + """ + for node in root.findall('targets'): + t = targets.TargetsFactory.new(packmap_, element=node) + for tgt in t.targets.values(): + callback(tgt) + + def get_target(self, target_name=None): + """Gets a target by name, or the default target if none is specified. + + Raises: + KeyError: if the spec does not have the requested target, + or the default target is requested but not defined. + """ + # Try to find a default target if not specified. + if not target_name: + if self.config.default_target: + target_name = self.config.default_target + else: + raise KeyError('{}: No default target could be found ' + 'in project spec. Possible targets are: ' + '{}.'.format(self.origin, self.targets.keys())) + + # Check that the desired target exists. + if target_name not in self.targets: + raise KeyError('{}: No such target "{}" (options are: {})'.format( + self.origin, target_name, self.targets.keys())) + + return self.targets[target_name] diff --git a/cli/lib/project/project_spec_stub.py b/cli/lib/project/project_spec_stub.py index 86a27bb..445de21 100644 --- a/cli/lib/project/project_spec_stub.py +++ b/cli/lib/project/project_spec_stub.py @@ -23,35 +23,35 @@ from project import config_stub class StubProjectSpec(object): - def __init__(self, targets=None, file_default_target='', file_targets=None, - should_read_file=None, packmap=None): - self.targets = targets or {} - self.file_default_target = file_default_target - self.file_targets = file_targets or {} - self.should_read_file = should_read_file or [] - self.packmap = packmap - self.config = None - - def add_packs(self, packs_obj): - self.packmap.update(packs_obj) - - def from_xml(self, f): - if not f in self.should_read_file: - raise IOError('Not supposed to read from file "{}"'.format(f)) - self.targets = self.file_targets - self.config = config_stub.StubConfig(self.file_default_target) - self.should_read_file.remove(f) - return self + def __init__(self, targets=None, file_default_target='', file_targets=None, + should_read_file=None, packmap=None): + self.targets = targets or {} + self.file_default_target = file_default_target + self.file_targets = file_targets or {} + self.should_read_file = should_read_file or [] + self.packmap = packmap + self.config = None + + def add_packs(self, packs_obj): + self.packmap.update(packs_obj) + + def from_xml(self, f): + if not f in self.should_read_file: + raise IOError('Not supposed to read from file "{}"'.format(f)) + self.targets = self.file_targets + self.config = config_stub.StubConfig(self.file_default_target) + self.should_read_file.remove(f) + return self class StubProjectSpecGenerator(object): - def __init__(self): - self.targets = {} - self.file_default_target = '' - self.file_targets = {} - self.should_read_file = [] - - @property - def ProjectSpec(self): - return StubProjectSpec(self.targets, self.file_default_target, - self.file_targets, self.should_read_file) + def __init__(self): + self.targets = {} + self.file_default_target = '' + self.file_targets = {} + self.should_read_file = [] + + @property + def ProjectSpec(self): + return StubProjectSpec(self.targets, self.file_default_target, + self.file_targets, self.should_read_file) diff --git a/cli/lib/project/project_spec_unittest.py b/cli/lib/project/project_spec_unittest.py index 3748aec..f105536 100644 --- a/cli/lib/project/project_spec_unittest.py +++ b/cli/lib/project/project_spec_unittest.py @@ -82,205 +82,208 @@ BDK_XML_TWO_TARGETS = ''' class ProjectSpecTest(unittest.TestCase): - def setUp(self): - os = StringIO.StringIO(MINIMAL_OS_XML) - board = StringIO.StringIO(MINIMAL_BOARD_XML) - self.minimal_board = packs.PacksFactory().new(file=board) - self.minimal_os = packs.PacksFactory().new(file=os) + def setUp(self): + os = StringIO.StringIO(MINIMAL_OS_XML) + board = StringIO.StringIO(MINIMAL_BOARD_XML) + self.minimal_board = packs.PacksFactory().new(file=board) + self.minimal_os = packs.PacksFactory().new(file=os) - def tearDown(self): - pass + def tearDown(self): + pass - def add_minimal_packs(self, project_spec_): - for ps in (self.minimal_os, self.minimal_board): - project_spec_.packmap.update(ps) + def add_minimal_packs(self, project_spec_): + for ps in (self.minimal_os, self.minimal_board): + project_spec_.packmap.update(ps) - def project_spec_from_str(self, xml): - s = StringIO.StringIO(xml) - b = project_spec.ProjectSpec.from_xml(s) - return b + def project_spec_from_str(self, xml): + s = StringIO.StringIO(xml) + b = project_spec.ProjectSpec.from_xml(s) + return b class ParseTest(ProjectSpecTest): - def test_minimal_ok(self): - spec = self.project_spec_from_str(MINIMAL_BDK_XML) - # Only 1 target, should become default target. - self.assertEqual(spec.config.default_target, 'sample.ledflasher') - - def test_two_targets_ok(self): - spec = self.project_spec_from_str(BDK_XML_TWO_TARGETS) - # 2 targets, neither should be the default. - self.assertFalse(spec.config.default_target) - - def test_minimal_populated(self): - b = self.project_spec_from_str(MINIMAL_BDK_XML) - self.add_minimal_packs(b) - b.packmap.report_missing() - - def test_minimal_no_os(self): - b = self.project_spec_from_str(MINIMAL_BDK_XML) - # There will be no global suppliers of "os.core". - with self.assertRaises(dependency.UndefinedPackError): - b.packmap.report_missing() - - def test_minimal_wrong_os_version(self): - b = self.project_spec_from_str(MINIMAL_BDK_XML) - # Create a mismatched os pack version. - self.minimal_os.namespace = 'brillo.2' - self.minimal_board.namespace = 'edison.2' - self.add_minimal_packs(b) - with self.assertRaises(dependency.UnsatisfiedVirtualPackError): - b.targets['sample.ledflasher'].create_submap(b.packmap) - - def test_minimal_wrong_board_version(self): - b = self.project_spec_from_str(MINIMAL_BDK_XML) - # Create a mismatched os pack version. - self.minimal_board.namespace = 'edison.2' - self.add_minimal_packs(b) - with self.assertRaises(dependency.UnsatisfiedVirtualPackError): - b.targets['sample.ledflasher'].create_submap(b.packmap) - - def test_minimal_requires_missing_pack(self): - xml = MINIMAL_BDK_XML.replace('os.core', 'missing.pack') - b = self.project_spec_from_str(xml) - # Create a mismatched os pack version. - self.add_minimal_packs(b) - with self.assertRaises(dependency.UndefinedPackError): - b.packmap.report_missing() - with self.assertRaises(dependency.UndefinedPackError): - b.targets['sample.ledflasher'].create_submap(b.packmap) - - def test_minimal_requires_overpopulated_os_virtual(self): - b = self.project_spec_from_str(MINIMAL_BDK_XML) - self.add_minimal_packs(b) - # Add an extra pack that provides os.core so that the - # autoselection for os prefix fails. - os = StringIO.StringIO(MINIMAL_OS_XML.replace( - 'all_of_brillo', 'the_other_half')) - os_dupe = packs.PacksFactory().new(file=os) - b.packmap.update(os_dupe) - # No global undefinedness errors. - b.packmap.report_missing() - with self.assertRaises(dependency.UnsatisfiedVirtualPackError): - b.targets['sample.ledflasher'].create_submap(b.packmap) - - def test_minimal_requires_unfulfilled_virtual(self): - b = self.project_spec_from_str( - MINIMAL_BDK_XML.replace('os.core', 'some.pack')) - self.add_minimal_packs(b) - # Create a single implementation for the virtual, but since it will - # not be autoselected by the os-prefix, an error will be raised that - # suggests this new pack: project_extras.all_of_brillo. - os = StringIO.StringIO(MINIMAL_OS_XML.replace( - 'brillo.1', 'project_extras').replace('os.core', 'some.pack')) - os_dupe = packs.PacksFactory().new(file=os) - b.packmap.update(os_dupe) - b.packmap.report_missing() - with self.assertRaises(dependency.UnsatisfiedVirtualPackError): - b.targets['sample.ledflasher'].create_submap(b.packmap) - - def test_bdk_complex_no_os(self): - p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') - b = project_spec.ProjectSpec.from_xml(p) - with self.assertRaises(dependency.UndefinedPackError) as upe: - # Take the default target and it should fail. - dfl = b.targets[b.config.default_target] - dfl.create_submap(b.packmap) - for line in str(upe.exception).split('\n'): - # Expect only undefined os packs. - self.assertIn('Undefined pack "os.', line) - - def test_bdk_complex_wrong_os(self): - p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') - b = project_spec.ProjectSpec.from_xml(p) - os_paths = ['schema/testcases/pass/brillo.12.xml', - 'schema/testcases/pass/brillo.12.common.xml'] - for p in os_paths: - p = util.GetBDKPath(p) - b.packmap.update(packs.PacksFactory().new(path=p)) - with self.assertRaises(dependency.UnsatisfiedVirtualPackError) as uvpe: - for tgt in b.targets.values(): - # Test all targets we didnt load an os for. - if tgt.os != 'brillo' or tgt.os_version != '12': - tgt.create_submap(b.packmap) - - for line in str(uvpe.exception).split('\n'): - # Expect candidates from brillo.12 because the first target is brillo.8 - # and will be unable to satisfy virtuals from the os. - self.assertIn('may satisfy the requirement: [\'brillo.12.', line) - - def test_bdk_complex_fulfilled(self): - p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') - b = project_spec.ProjectSpec.from_xml(p) - os_paths = ['schema/testcases/pass/brillo.12.xml', - 'schema/testcases/pass/brillo.12.common.xml', - 'schema/testcases/pass/brillo.8.xml', - 'schema/testcases/pass/brillo.8.common.xml'] - for p in os_paths: - p = util.GetBDKPath(p) - b.packmap.update(packs.PacksFactory().new(path=p)) - # No undefined packs - b.packmap.report_missing() - expected_target_names = ['doorman_ed', 'doorman_db', 'doorman_ab', - 'doorman_2_db', 'doorman_2_ed', 'doorman_2_ab', - 'doorman_2_cba'] - self.assertEqual(expected_target_names, b.targets.keys()) - expected_doorman_2_cba_packs = [ - 'speech.synth_service', '3p.opencv.libopencv', 'products.doorman', - 'soc.hw.vision.libface_offload', 'brillo.12.0.0.audiorec', - 'products.doorman_v2', '3p.cloud.voice_api', 'products.doorman_v2_hw', - 'speech.local_synth', 'brillo.12.0.0.core_stuff', 'cam.facerecr_v2', - 'mic.cloud_voicerec', 'mic.local_voicerec', - 'brillo.12.0.0.new_super_cam', '3p.sndobj.libsndobj', - 'brillo.12.0.0.peripheral_iod'].sort() - t = b.targets['doorman_2_cba'] - tgt_map = t.create_submap(b.packmap) - self.assertEqual(expected_doorman_2_cba_packs, tgt_map.map.keys().sort()) - expected_files = [ - '/system/bin/cloud_voiced', '/lib/libfacerecr-2.0.so', - '/system/etc/init.d/local_voiced.init', '/lib/facerecrd', - '/system/etc/init.d/cloud_voiced.init', '/system/bin/', - '/system/lib/libsndobj-1.2.so', '/data/voice_api.wsdl', - '/system/lib/libcloudvoice.so', '/data/speech/training/', - '/system/bin/local_voiced', '/system/lib/libspeech-be.so', - '/system/etc/init.d/facerecerd.init', - '/system/lib/libface_offload.so'].sort() - self.assertEqual(expected_files, tgt_map.copy_destinations.keys().sort()) - # Make sure all other packs are satisfied. - for tgt in b.targets.values(): - tgt.create_submap(b.packmap) + def test_minimal_ok(self): + spec = self.project_spec_from_str(MINIMAL_BDK_XML) + # Only 1 target, should become default target. + self.assertEqual(spec.config.default_target, 'sample.ledflasher') + + def test_two_targets_ok(self): + spec = self.project_spec_from_str(BDK_XML_TWO_TARGETS) + # 2 targets, neither should be the default. + self.assertFalse(spec.config.default_target) + + def test_minimal_populated(self): + b = self.project_spec_from_str(MINIMAL_BDK_XML) + self.add_minimal_packs(b) + b.packmap.report_missing() + + def test_minimal_no_os(self): + b = self.project_spec_from_str(MINIMAL_BDK_XML) + # There will be no global suppliers of "os.core". + with self.assertRaises(dependency.UndefinedPackError): + b.packmap.report_missing() + + def test_minimal_wrong_os_version(self): + b = self.project_spec_from_str(MINIMAL_BDK_XML) + # Create a mismatched os pack version. + self.minimal_os.namespace = 'brillo.2' + self.minimal_board.namespace = 'edison.2' + self.add_minimal_packs(b) + with self.assertRaises(dependency.UnsatisfiedVirtualPackError): + b.targets['sample.ledflasher'].create_submap(b.packmap) + + def test_minimal_wrong_board_version(self): + b = self.project_spec_from_str(MINIMAL_BDK_XML) + # Create a mismatched os pack version. + self.minimal_board.namespace = 'edison.2' + self.add_minimal_packs(b) + with self.assertRaises(dependency.UnsatisfiedVirtualPackError): + b.targets['sample.ledflasher'].create_submap(b.packmap) + + def test_minimal_requires_missing_pack(self): + xml = MINIMAL_BDK_XML.replace('os.core', 'missing.pack') + b = self.project_spec_from_str(xml) + # Create a mismatched os pack version. + self.add_minimal_packs(b) + with self.assertRaises(dependency.UndefinedPackError): + b.packmap.report_missing() + with self.assertRaises(dependency.UndefinedPackError): + b.targets['sample.ledflasher'].create_submap(b.packmap) + + def test_minimal_requires_overpopulated_os_virtual(self): + b = self.project_spec_from_str(MINIMAL_BDK_XML) + self.add_minimal_packs(b) + # Add an extra pack that provides os.core so that the + # autoselection for os prefix fails. + os = StringIO.StringIO(MINIMAL_OS_XML.replace( + 'all_of_brillo', 'the_other_half')) + os_dupe = packs.PacksFactory().new(file=os) + b.packmap.update(os_dupe) + # No global undefinedness errors. + b.packmap.report_missing() + with self.assertRaises(dependency.UnsatisfiedVirtualPackError): + b.targets['sample.ledflasher'].create_submap(b.packmap) + + def test_minimal_requires_unfulfilled_virtual(self): + b = self.project_spec_from_str( + MINIMAL_BDK_XML.replace('os.core', 'some.pack')) + self.add_minimal_packs(b) + # Create a single implementation for the virtual, but since it will + # not be autoselected by the os-prefix, an error will be raised that + # suggests this new pack: project_extras.all_of_brillo. + os = StringIO.StringIO(MINIMAL_OS_XML.replace( + 'brillo.1', 'project_extras').replace('os.core', 'some.pack')) + os_dupe = packs.PacksFactory().new(file=os) + b.packmap.update(os_dupe) + b.packmap.report_missing() + with self.assertRaises(dependency.UnsatisfiedVirtualPackError): + b.targets['sample.ledflasher'].create_submap(b.packmap) + + def test_bdk_complex_no_os(self): + p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') + b = project_spec.ProjectSpec.from_xml(p) + with self.assertRaises(dependency.UndefinedPackError) as upe: + # Take the default target and it should fail. + dfl = b.targets[b.config.default_target] + dfl.create_submap(b.packmap) + for line in str(upe.exception).split('\n'): + # Expect only undefined os packs. + self.assertIn('Undefined pack "os.', line) + + def test_bdk_complex_wrong_os(self): + p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') + b = project_spec.ProjectSpec.from_xml(p) + os_paths = ['schema/testcases/pass/brillo.12.xml', + 'schema/testcases/pass/brillo.12.common.xml'] + for p in os_paths: + p = util.GetBDKPath(p) + b.packmap.update(packs.PacksFactory().new(path=p)) + with self.assertRaises(dependency.UnsatisfiedVirtualPackError) as uvpe: + for tgt in b.targets.values(): + # Test all targets we didnt load an os for. + if tgt.os != 'brillo' or tgt.os_version != '12': + tgt.create_submap(b.packmap) + + for line in str(uvpe.exception).split('\n'): + # Expect candidates from brillo.12 because the first target is + # brillo.8 and will be unable to satisfy virtuals from the os. + self.assertIn('may satisfy the requirement: [\'brillo.12.', line) + + def test_bdk_complex_fulfilled(self): + p = util.GetBDKPath('schema/testcases/pass/bdk_complex.xml') + b = project_spec.ProjectSpec.from_xml(p) + os_paths = ['schema/testcases/pass/brillo.12.xml', + 'schema/testcases/pass/brillo.12.common.xml', + 'schema/testcases/pass/brillo.8.xml', + 'schema/testcases/pass/brillo.8.common.xml'] + for p in os_paths: + p = util.GetBDKPath(p) + b.packmap.update(packs.PacksFactory().new(path=p)) + # No undefined packs + b.packmap.report_missing() + expected_target_names = ['doorman_ed', 'doorman_db', 'doorman_ab', + 'doorman_2_db', 'doorman_2_ed', 'doorman_2_ab', + 'doorman_2_cba'] + self.assertEqual(expected_target_names, b.targets.keys()) + expected_doorman_2_cba_packs = [ + 'speech.synth_service', '3p.opencv.libopencv', 'products.doorman', + 'soc.hw.vision.libface_offload', 'brillo.12.0.0.audiorec', + 'products.doorman_v2', '3p.cloud.voice_api', + 'products.doorman_v2_hw', 'speech.local_synth', + 'brillo.12.0.0.core_stuff', 'cam.facerecr_v2', + 'mic.cloud_voicerec', 'mic.local_voicerec', + 'brillo.12.0.0.new_super_cam', '3p.sndobj.libsndobj', + 'brillo.12.0.0.peripheral_iod'].sort() + t = b.targets['doorman_2_cba'] + tgt_map = t.create_submap(b.packmap) + self.assertEqual(expected_doorman_2_cba_packs, + tgt_map.map.keys().sort()) + expected_files = [ + '/system/bin/cloud_voiced', '/lib/libfacerecr-2.0.so', + '/system/etc/init.d/local_voiced.init', '/lib/facerecrd', + '/system/etc/init.d/cloud_voiced.init', '/system/bin/', + '/system/lib/libsndobj-1.2.so', '/data/voice_api.wsdl', + '/system/lib/libcloudvoice.so', '/data/speech/training/', + '/system/bin/local_voiced', '/system/lib/libspeech-be.so', + '/system/etc/init.d/facerecerd.init', + '/system/lib/libface_offload.so'].sort() + self.assertEqual(expected_files, + tgt_map.copy_destinations.keys().sort()) + # Make sure all other packs are satisfied. + for tgt in b.targets.values(): + tgt.create_submap(b.packmap) class ProjectSpecMethodsTest(unittest.TestCase): - def setUp(self): - self.spec = project_spec.ProjectSpec() - self.target_a = target_stub.StubTarget('a') - self.spec.add_target(self.target_a) - self.target_b = target_stub.StubTarget('b') - self.spec.add_target(self.target_b) - self.spec.config = config_stub.StubConfig(default_target='b') - - def test_get_target(self): - self.assertEqual(self.spec.get_target('a'), self.target_a) - - def test_get_default_target(self): - self.assertEqual(self.spec.get_target(), self.target_b) - - def test_get_target_invalid(self): - with self.assertRaises(KeyError): - self.spec.get_target('c') - - def test_get_target_no_default(self): - self.spec.config.default_target = None - with self.assertRaises(KeyError): - self.spec.get_target() - - def test_add_target(self): - target_c = target_stub.StubTarget('c') - self.spec.add_target(target_c) - self.assertEqual(self.spec.get_target('c'), target_c) - - def test_add_target_name_conflict(self): - target_b_2 = target_stub.StubTarget('b') - with self.assertRaises(common.LoadErrorWithOrigin): - self.spec.add_target(target_b_2) + def setUp(self): + self.spec = project_spec.ProjectSpec() + self.target_a = target_stub.StubTarget('a') + self.spec.add_target(self.target_a) + self.target_b = target_stub.StubTarget('b') + self.spec.add_target(self.target_b) + self.spec.config = config_stub.StubConfig(default_target='b') + + def test_get_target(self): + self.assertEqual(self.spec.get_target('a'), self.target_a) + + def test_get_default_target(self): + self.assertEqual(self.spec.get_target(), self.target_b) + + def test_get_target_invalid(self): + with self.assertRaises(KeyError): + self.spec.get_target('c') + + def test_get_target_no_default(self): + self.spec.config.default_target = None + with self.assertRaises(KeyError): + self.spec.get_target() + + def test_add_target(self): + target_c = target_stub.StubTarget('c') + self.spec.add_target(target_c) + self.assertEqual(self.spec.get_target('c'), target_c) + + def test_add_target_name_conflict(self): + target_b_2 = target_stub.StubTarget('b') + with self.assertRaises(common.LoadErrorWithOrigin): + self.spec.add_target(target_b_2) diff --git a/cli/lib/project/target.py b/cli/lib/project/target.py index 9a28d65..719b6d8 100644 --- a/cli/lib/project/target.py +++ b/cli/lib/project/target.py @@ -21,7 +21,6 @@ import os from bsp import manifest from core import user_config -from core import util from project import common @@ -33,156 +32,162 @@ PLATFORM_CACHE_FORMAT = os.path.join('{user_configured_platform_cache}', class Error(common.Error): - """General Error for targets.""" + """General Error for targets.""" class BoardError(Error): - """Raised when there is a problem with the specified board/version.""" - description = 'Invalid board' + """Raised when there is a problem with the specified board/version.""" + description = 'Invalid board' class Target(object): - """A target for a project. - - Specifies what a target consists of, and what platform - it is intended for. - """ - - def __init__(self, name, os='', os_version='', board='', board_version=''): - self._name = name - self._pack = None - self._pack_name = '' - self._os = os - self._os_version = os_version - self._board = board - self._board_version = board_version - self._build = 'userdebug' - self._origin = common.Origin() - - def __repr__(self): - return ('<target name="{}" os="{}" os-version="{}" ' - 'board="{}" board-version="{}" build="{}" pack="{}"/>').format( - self._name, self._os, self._os_version, self._board, - self._board_version, self._build, self._pack_name) - - @property - def name(self): - return self._name - - @property - def pack_name(self): - return self._pack_name - - @property - def pack(self): - return self._pack - - @property - def origin(self): - return self._origin - - @property - def os(self): - return self._os - - @os.setter - def os(self, b): - self._os = b - - @property - def os_namespace(self): - return '{}.{}'.format(self.os, self.os_version) - - @property - def os_version(self): - return self._os_version - - @os_version.setter - def os_version(self, b): - self._os_version = b - - @property - def board(self): - return self._board - - @board.setter - def board(self, b): - self._board = b - - @property - def board_version(self): - return self._board_version - - @board_version.setter - def board_version(self, b): - self._board_version = b - - @property - def board_namespace(self): - return '{}.{}'.format(self.board, self.board_version) - - @property - def build_type(self): - return self._build - - def get_device(self): - """Get the bsp.Device associated with this Target. - - Returns: - The bsp.Device associated with this Target. - - Raises: - BoardError: If the target board and version is not in the manifest. - """ - bsp_manifest = manifest.Manifest.from_json() - board = bsp_manifest.devices.get(self.board) - if not board: - raise BoardError('unrecognized name "{}". ' - 'Run `bdk bsp list` to see available boards.'.format( - self.board)) - if board.version != self.board_version: - raise BoardError('"{}" only has version {} (requested {}).'.format( - self.board, board.version, self.board_version)) - return board - - def platform_cache(self, *relpath): - return os.path.join(PLATFORM_CACHE_FORMAT.format( - user_configured_platform_cache=user_config.USER_CONFIG.platform_cache, - os=self.os, - os_version=self.os_version, - board=self.board, - board_version=self.board_version), - *relpath) - - def platform_build_cache(self, *relpath): - return self.platform_cache(self.build_type, *relpath) - - def load(self, ele): - """Populates this instance from Element @ele.""" - self._origin = ele.origin.copy() - ele.limit_attribs(['name', 'pack', 'os', 'board', - 'os-version', 'board-version', 'build']) - self._pack_name = ele.get_attrib('pack') - self._os = ele.get_attrib('os') - self._os_version = ele.get_attrib('os-version') - self._board = ele.get_attrib('board') - self._board_version = ele.get_attrib('board-version') - if 'build' in ele.attrib: - self._build = ele.get_attrib('build') - - def create_submap(self, global_packmap): - """Create a packmap from the global packmap. - - Prior to calling, populate the global packmap with any - requires OS or Board packs. - - Args: - global_packmap: A packmap of all available packs. - - Returns: - A submap of global_packmap for this target, with paths validated. + """A target for a project. + + Specifies what a target consists of, and what platform + it is intended for. """ - aliases = {'os': self.os_namespace, 'board': self.board_namespace} - packmap = global_packmap.submap(self._pack_name, aliases) - packmap.check_paths() - return packmap + + # TODO(b/28296932): Rename the 'os' input variable to not conflict with the + # import. + # pylint: disable=redefined-outer-name + def __init__(self, name, os='', os_version='', board='', + board_version=''): + self._name = name + self._pack = None + self._pack_name = '' + self._os = os + self._os_version = os_version + self._board = board + self._board_version = board_version + self._build = 'userdebug' + self._origin = common.Origin() + + def __repr__(self): + return ('<target name="{}" os="{}" os-version="{}" ' + 'board="{}" board-version="{}" build="{}" pack="{}"/>').format( + self._name, self._os, self._os_version, self._board, + self._board_version, self._build, self._pack_name) + + @property + def name(self): + return self._name + + @property + def pack_name(self): + return self._pack_name + + @property + def pack(self): + return self._pack + + @property + def origin(self): + return self._origin + + @property + def os(self): + return self._os + + @os.setter + def os(self, b): + self._os = b + + @property + def os_namespace(self): + return '{}.{}'.format(self.os, self.os_version) + + @property + def os_version(self): + return self._os_version + + @os_version.setter + def os_version(self, b): + self._os_version = b + + @property + def board(self): + return self._board + + @board.setter + def board(self, b): + self._board = b + + @property + def board_version(self): + return self._board_version + + @board_version.setter + def board_version(self, b): + self._board_version = b + + @property + def board_namespace(self): + return '{}.{}'.format(self.board, self.board_version) + + @property + def build_type(self): + return self._build + + def get_device(self): + """Get the bsp.Device associated with this Target. + + Returns: + The bsp.Device associated with this Target. + + Raises: + BoardError: If the target board and version is not in the manifest. + """ + bsp_manifest = manifest.Manifest.from_json() + board = bsp_manifest.devices.get(self.board) + if not board: + raise BoardError('unrecognized name "{}". ' + 'Run `bdk bsp list` to see available ' + 'boards.'.format(self.board)) + if board.version != self.board_version: + raise BoardError('"{}" only has version {} (requested {}).'.format( + self.board, board.version, self.board_version)) + return board + + def platform_cache(self, *relpath): + return os.path.join( + PLATFORM_CACHE_FORMAT.format( + user_configured_platform_cache=( + user_config.USER_CONFIG.platform_cache), + os=self.os, + os_version=self.os_version, + board=self.board, + board_version=self.board_version), + *relpath) + + def platform_build_cache(self, *relpath): + return self.platform_cache(self.build_type, *relpath) + + def load(self, ele): + """Populates this instance from Element @ele.""" + self._origin = ele.origin.copy() + ele.limit_attribs(['name', 'pack', 'os', 'board', + 'os-version', 'board-version', 'build']) + self._pack_name = ele.get_attrib('pack') + self._os = ele.get_attrib('os') + self._os_version = ele.get_attrib('os-version') + self._board = ele.get_attrib('board') + self._board_version = ele.get_attrib('board-version') + if 'build' in ele.attrib: + self._build = ele.get_attrib('build') + + def create_submap(self, global_packmap): + """Create a packmap from the global packmap. + + Prior to calling, populate the global packmap with any + requires OS or Board packs. + + Args: + global_packmap: A packmap of all available packs. + + Returns: + A submap of global_packmap for this target, with paths validated. + """ + aliases = {'os': self.os_namespace, 'board': self.board_namespace} + packmap = global_packmap.submap(self._pack_name, aliases) + packmap.check_paths() + return packmap diff --git a/cli/lib/project/target_stub.py b/cli/lib/project/target_stub.py index 2f4f993..f236533 100644 --- a/cli/lib/project/target_stub.py +++ b/cli/lib/project/target_stub.py @@ -20,53 +20,55 @@ import os -from core import util from project import target class StubTarget(object): - def __init__(self, name='', os='', os_version='', - board='', board_version='', build_type='', - origin='test_target_origin', - platform_dir=None, - device_raises=False, device=None, - submap_raises=None, submaps=None): - self.name = name - self.os = os - self.os_version = os_version - self.os_namespace = '{}.{}'.format(os, os_version) - self.board = board - self.board_version = board_version - self.board_namespace = '{}.{}'.format(board, board_version) - self.build_type = build_type - self.origin = origin - self.device = device - self.platform_dir = platform_dir + # TODO(b/28296932): Rename the 'os' input variable to not conflict with the + # import. + # pylint: disable=redefined-outer-name + def __init__(self, name='', os='', os_version='', + board='', board_version='', build_type='', + origin='test_target_origin', + platform_dir=None, + device_raises=False, device=None, + submap_raises=None, submaps=None): + self.name = name + self.os = os + self.os_version = os_version + self.os_namespace = '{}.{}'.format(os, os_version) + self.board = board + self.board_version = board_version + self.board_namespace = '{}.{}'.format(board, board_version) + self.build_type = build_type + self.origin = origin + self.device = device + self.platform_dir = platform_dir - self.device_raises = device_raises - self._submap_raises = submap_raises - self._submaps = submaps or [] + self.device_raises = device_raises + self._submap_raises = submap_raises + self._submaps = submaps or [] - def get_device(self): - if self.device_raises: - raise target.BoardError - return self.device + def get_device(self): + if self.device_raises: + raise target.BoardError + return self.device - def platform_cache(self, *relpath): - return os.path.join(self.platform_dir, *relpath) + def platform_cache(self, *relpath): + return os.path.join(self.platform_dir, *relpath) - def platform_build_cache(self, *relpath): - return self.platform_cache(self.build_type, *relpath) + def platform_build_cache(self, *relpath): + return self.platform_cache(self.build_type, *relpath) - def create_submap(self, _packmap): - if self._submap_raises: - # Pylint issues an error about raising NoneType, since self._submap_raises - # is set to None by default and pylint doesn't recognize that the raise - # call is only called if it is not None. In the case that it is set to - # a non-Exception, running the unit test should raise a TypeError and - # (correctly) fail the unit test, so it should be safe to just disable - # the pylint check here. - # pylint: disable=raising-bad-type - raise self._submap_raises - return self._submaps.pop(0) + def create_submap(self, _packmap): + if self._submap_raises: + # Pylint issues an error about raising NoneType, since + # self._submap_raises is set to None by default and pylint doesn't + # recognize that the raise call is only called if it is not None. + # In the case that it is set to a non-Exception, running the unit + # test should raise a TypeError and (correctly) fail the unit test, + # so it should be safe to just disable the pylint check here. + # pylint: disable=raising-bad-type + raise self._submap_raises + return self._submaps.pop(0) diff --git a/cli/lib/project/targets.py b/cli/lib/project/targets.py index 05bd817..b7dde51 100644 --- a/cli/lib/project/targets.py +++ b/cli/lib/project/targets.py @@ -24,59 +24,60 @@ from project import target class TargetsFactory(object): - @staticmethod - def new(packmap, **kwargs): - """Creates a new Targets instance from an XML file or element. + @staticmethod + def new(packmap, **kwargs): + """Creates a new Targets instance from an XML file or element. - Walks an ElementTree <target> looking for <target> child - nodes and instantiates Target objects. + Walks an ElementTree <target> looking for <target> child + nodes and instantiates Target objects. - Args: - packmap: A PackMap containing all necessary Packs for the parsed targets. - **kwargs: Any valid keywords for loader.PathElementLoader.load() + Args: + packmap: A PackMap containing all necessary Packs for the parsed + targets. + **kwargs: Any valid keywords for loader.PathElementLoader.load() - Returns: - new Targets() instance + Returns: + new Targets() instance - Raises - LoadError: LoadError, or a subclass, will be raised if an error occurs - loading and validating content from the XML. - """ - ts = Targets() - l = loader.PathElementLoader('targets', []) - root = l.load(**kwargs) - root.limit_attribs(['version', 'path']) - targets = {} - for node in root.findall('target'): - # The pack name is the default target name. - name = node.get_attrib('pack') - if 'name' in node.attrib: - name = node.get_attrib('name') - if name in targets: - raise common.LoadErrorWithOrigin( - node, - 'Target "{}" redefined. Previously defined here: "{}".'.format( - name, targets[name].origin)) - t = target.Target(name) - t.load(node) - if t.pack_name not in packmap.map: - raise common.LoadErrorWithOrigin( - t.origin, - 'Target "{}" specifies undefined pack "{}"'.format( - t.name, - t.pack_name)) - targets[name] = t + Raises + LoadError: LoadError, or a subclass, will be raised if an error + occurs loading and validating content from the XML. + """ + ts = Targets() + l = loader.PathElementLoader('targets', []) + root = l.load(**kwargs) + root.limit_attribs(['version', 'path']) + targets = {} + for node in root.findall('target'): + # The pack name is the default target name. + name = node.get_attrib('pack') + if 'name' in node.attrib: + name = node.get_attrib('name') + if name in targets: + raise common.LoadErrorWithOrigin( + node, + ('Target "{}" redefined. Previously defined here: ' + '"{}".'.format(name, targets[name].origin))) + t = target.Target(name) + t.load(node) + if t.pack_name not in packmap.map: + raise common.LoadErrorWithOrigin( + t.origin, + 'Target "{}" specifies undefined pack "{}"'.format( + t.name, + t.pack_name)) + targets[name] = t - ts.origins = l.origins - ts.entries = targets - return ts + ts.origins = l.origins + ts.entries = targets + return ts class Targets(collection.Base): - """Collection of Target objects.""" - def __init__(self): - super(Targets, self).__init__('targets') + """Collection of Target objects.""" + def __init__(self): + super(Targets, self).__init__('targets') - @property - def targets(self): - return self.entries + @property + def targets(self): + return self.entries diff --git a/cli/lib/project/xml_parser.py b/cli/lib/project/xml_parser.py index 131d6c1..d467889 100644 --- a/cli/lib/project/xml_parser.py +++ b/cli/lib/project/xml_parser.py @@ -34,73 +34,80 @@ from project import common class ElementExtras(object): - @staticmethod - def get_attrib(node, key): - if key not in node.attrib: - raise common.MissingAttribute( - node.origin, '{} lacks a @{} attribute'.format(node.tag, key)) - val = node.attrib.get(key) - if len(val) == 0: - raise common.MissingAttribute( - node.origin, - '<{}>\'s @{} attribute is empty'.format(node.tag, key)) - return val - - @staticmethod - def limit_attribs(node, keys): - bad_keys = set(node.attrib) - set(keys) - if len(bad_keys): - raise common.UnknownAttributes( - node.origin, - '{} contains unsupported attributes: {} '.format(node.tag, bad_keys)) - - @staticmethod - def extend_element(): - ET.Element.get_attrib = ElementExtras.get_attrib - ET.Element.limit_attribs = ElementExtras.limit_attribs + @staticmethod + def get_attrib(node, key): + if key not in node.attrib: + raise common.MissingAttribute( + node.origin, '{} lacks a @{} attribute'.format(node.tag, key)) + val = node.attrib.get(key) + if len(val) == 0: + raise common.MissingAttribute( + node.origin, + '<{}>\'s @{} attribute is empty'.format(node.tag, key)) + return val + + @staticmethod + def limit_attribs(node, keys): + bad_keys = set(node.attrib) - set(keys) + if len(bad_keys): + raise common.UnknownAttributes( + node.origin, + '{} contains unsupported attributes: {} '.format(node.tag, + bad_keys)) + + @staticmethod + def extend_element(): + ET.Element.get_attrib = ElementExtras.get_attrib + ET.Element.limit_attribs = ElementExtras.limit_attribs class AnnotatedXMLTreeBuilder(XMLTreeBuilder): - def __init__(self): - super(AnnotatedXMLTreeBuilder, self).__init__() - self.file_name = None - - def _start_fixup(self, element): - """Adds element source annotation as the ElementTree is built.""" - # Grab the line/col from expat, but expect the path - # to be passed in. - element.origin = common.Origin(self.file_name, - self._parser.CurrentLineNumber, - self._parser.CurrentColumnNumber) - return element - - def _start(self, tag, attrib_in): - """Thunks the XMLTreeBuilder _start adding the element annotations.""" - # If _start_list was supported, then we'll see a list and should call over. - if type(attrib_in) == list: - return self._start_list(tag, attrib_in) - - element = super(AnnotatedXMLTreeBuilder, self)._start(tag, attrib_in) - return self._start_fixup(element) - - def _start_list(self, tag, attrib_in): - """Thunks the XMLTreeBuilder _start_list adding the element annotations.""" - element = super(AnnotatedXMLTreeBuilder, self)._start_list(tag, attrib_in) - return self._start_fixup(element) - - def subclass_fixups(self): - """Updates the parser callback to the _start element in this instance.""" - # This may clobber start_list, but we hide that in _start. - self._parser.StartElementHandler = self._start + def __init__(self): + super(AnnotatedXMLTreeBuilder, self).__init__() + self.file_name = None + + def _start_fixup(self, element): + """Adds element source annotation as the ElementTree is built.""" + # Grab the line/col from expat, but expect the path + # to be passed in. + element.origin = common.Origin(self.file_name, + self._parser.CurrentLineNumber, + self._parser.CurrentColumnNumber) + return element + + def _start(self, tag, attrib_in): + """Thunks the XMLTreeBuilder _start adding the element annotations.""" + # If _start_list was supported, then we'll see a list and should call + # over. + if type(attrib_in) == list: + return self._start_list(tag, attrib_in) + + element = super(AnnotatedXMLTreeBuilder, self)._start(tag, attrib_in) + return self._start_fixup(element) + + def _start_list(self, tag, attrib_in): + """Thunks the XMLTreeBuilder _start_list adding the element + annotations. + """ + element = super(AnnotatedXMLTreeBuilder, self)._start_list(tag, + attrib_in) + return self._start_fixup(element) + + def subclass_fixups(self): + """Updates the parser callback to the _start element in this + instance. + """ + # This may clobber start_list, but we hide that in _start. + self._parser.StartElementHandler = self._start def parse(path): - """Replaces ET.parse() as an entry point for adding source annotations.""" - ElementExtras.extend_element() - parser = AnnotatedXMLTreeBuilder() - parser.file_name = str(path) - if hasattr(path, 'name'): - parser.file_name = path.name - parser.subclass_fixups() - return ET.parse(path, parser=parser) + """Replaces ET.parse() as an entry point for adding source annotations.""" + ElementExtras.extend_element() + parser = AnnotatedXMLTreeBuilder() + parser.file_name = str(path) + if hasattr(path, 'name'): + parser.file_name = path.name + parser.subclass_fixups() + return ET.parse(path, parser=parser) |