blob: 3315b66b246839a188781d239ff3ce2c8b25e8ac [file] [log] [blame]
#
# Copyright (C) 2016 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""This file defines PackMap and associated support classes."""
import os
from collections import defaultdict
from project import common
from project import dependency
from project import packs
class UpdateError(common.LoadErrorWithOrigin):
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.
"""
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())