| # |
| # 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()) |