1# Copyright (c) 2011, Willow Garage, Inc.
2# All rights reserved.
3#
4# Redistribution and use in source and binary forms, with or without
5# modification, are permitted provided that the following conditions are met:
6#
7#     * Redistributions of source code must retain the above copyright
8#       notice, this list of conditions and the following disclaimer.
9#     * Redistributions in binary form must reproduce the above copyright
10#       notice, this list of conditions and the following disclaimer in the
11#       documentation and/or other materials provided with the distribution.
12#     * Neither the name of the Willow Garage, Inc. nor the names of its
13#       contributors may be used to endorse or promote products derived from
14#       this software without specific prior written permission.
15#
16# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
17# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
18# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
19# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
20# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
21# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
22# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
23# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
24# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
25# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
26# POSSIBILITY OF SUCH DAMAGE.
27
28# Author Tully Foote/tfoote@willowgarage.com, Ken Conley/kwc@willowgarage.com
29
30from __future__ import print_function
31
32import sys
33import yaml
34
35from collections import defaultdict
36
37from rospkg import RosPack, RosStack, ResourceNotFound
38
39from .core import RosdepInternalError, InvalidData, rd_debug
40from .model import RosdepDatabase
41from .rospkg_loader import RosPkgLoader
42from .dependency_graph import DependencyGraph
43
44from .sources_list import SourcesListLoader
45
46from . import catkin_packages
47
48
49class RosdepDefinition(object):
50    """
51    Single rosdep dependency definition.  This data is stored as the
52    raw dictionary definition for the dependency.
53
54    See REP 111, 'Multiple Package Manager Support for Rosdep' for a
55    discussion of this raw format.
56    """
57
58    def __init__(self, rosdep_key, data, origin='<dynamic>'):
59        """
60        :param rosdep_key: key/name of rosdep dependency
61        :param data: raw rosdep data for a single rosdep dependency, ``dict``
62        :param origin: string that indicates where data originates from (e.g. filename)
63        """
64        self.rosdep_key = rosdep_key
65        if not isinstance(data, dict):
66            raise InvalidData('rosdep data for [%s] must be a dictionary' % (self.rosdep_key), origin=origin)
67        self.data = data
68        self.origin = origin
69
70    def reverse_merge(self, new_data, origin='<dynamic>', verbose=False):
71        """
72        Merge two definitions together, with existing rules taking precendence.
73        Definitions are merged at the os_name level, meaning that if two rules
74        exist with the same os_name, the first one wins.
75
76        :param data: raw rosdep data for a single rosdep dependency, ``dict``
77        :param origin: string that indicates where this new data comes from (e.g. filename)
78        """
79        for os_name, rules in new_data.items():
80            if os_name not in self.data:
81                if verbose:
82                    print('[%s] adding rules for os [%s] to [%s]' % (origin, os_name, self.rosdep_key), file=sys.stderr)
83                self.data[os_name] = rules
84            elif verbose:
85                print('[%s] ignoring [%s] for os [%s], already loaded' % (origin, self.rosdep_key, os_name), file=sys.stderr)
86
87    def get_rule_for_platform(self, os_name, os_version, installer_keys, default_installer_key):
88        """
89        Get installer_key and rule for the specified rule.  See REP 111 for precedence rules.
90
91        :param os_name: OS name to get rule for
92        :param os_version: OS version to get rule for
93        :param installer_keys: Keys of installers for platform, ``[str]``
94        :param default_installer_key: Default installer key for platform, ``[str]``
95        :returns: (installer_key, rosdep_args_dict), ``(str, dict)``
96
97        :raises: :exc:`ResolutionError` If no rule is available
98        :raises: :exc:`InvalidData` If rule data is not valid
99        """
100        rosdep_key = self.rosdep_key
101        data = self.data
102
103        if type(data) != dict:
104            raise InvalidData('rosdep value for [%s] must be a dictionary' % (self.rosdep_key), origin=self.origin)
105        if os_name not in data:
106            raise ResolutionError(rosdep_key, data, os_name, os_version, 'No definition of [%s] for OS [%s]' % (rosdep_key, os_name))
107        data = data[os_name]
108        return_key = default_installer_key
109
110        # REP 111: rosdep first interprets the key as a
111        # PACKAGE_MANAGER. If this test fails, it will be interpreted
112        # as an OS_VERSION_CODENAME.
113        if type(data) == dict:
114            for installer_key in installer_keys:
115                if installer_key in data:
116                    data = data[installer_key]
117                    return_key = installer_key
118                    break
119            else:
120                # data must be a dictionary, string, or list
121                if type(data) == dict:
122                    # check for
123                    #   hardy:
124                    #     apt:
125                    #       stuff
126
127                    # we've already checked for PACKAGE_MANAGER_KEY, so
128                    # version key must be present here for data to be valid
129                    # dictionary value.
130                    # if the os_version is not defined and there is no wildcard
131                    if os_version not in data and '*' not in data:
132                        raise ResolutionError(rosdep_key, self.data, os_name, os_version, 'No definition of [%s] for OS version [%s]' % (rosdep_key, os_version))
133                    # if the os_version has the value None
134                    if os_version in data and data[os_version] is None:
135                        raise ResolutionError(rosdep_key, self.data, os_name, os_version, '[%s] defined as "not available" for OS version [%s]' % (rosdep_key, os_version))
136                    # if os version is not defined (and there is a wildcard) fallback to the wildcard
137                    if os_version not in data:
138                        os_version = '*'
139                    data = data[os_version]
140                    if type(data) == dict:
141                        for installer_key in installer_keys:
142                            if installer_key in data:
143                                data = data[installer_key]
144                                return_key = installer_key
145                                break
146
147        if type(data) not in (dict, list, type('str')):
148            raise InvalidData('rosdep OS definition for [%s:%s] must be a dictionary, string, or list: %s' % (self.rosdep_key, os_name, data), origin=self.origin)
149
150        return return_key, data
151
152    def __str__(self):
153        return '%s:\n%s' % (self.origin, yaml.dump(self.data, default_flow_style=False))
154
155
156class ResolutionError(Exception):
157
158    def __init__(self, rosdep_key, rosdep_data, os_name, os_version, message):
159        self.rosdep_key = rosdep_key
160        self.rosdep_data = rosdep_data
161        self.os_name = os_name
162        self.os_version = os_version
163        super(ResolutionError, self).__init__(message)
164
165    def __str__(self):
166        if self.rosdep_data:
167            pretty_data = yaml.dump(self.rosdep_data, default_flow_style=False)
168        else:
169            pretty_data = '<no data>'
170        return """%s
171\trosdep key : %s
172\tOS name    : %s
173\tOS version : %s
174\tData: %s""" % (self.args[0], self.rosdep_key, self.os_name, self.os_version, pretty_data)
175
176
177class RosdepView(object):
178    """
179    View of :class:`RosdepDatabase`.  Unlike :class:`RosdepDatabase`,
180    which stores :class:`RosdepDatabaseEntry` data for all stacks, a
181    view merges entries for a particular stack.  This view can then be
182    queried to lookup and resolve individual rosdep dependencies.
183    """
184
185    def __init__(self, name):
186        self.name = name
187        self.rosdep_defs = {}  # {str: RosdepDefinition}
188
189    def __str__(self):
190        return '\n'.join(['%s: %s' % val for val in self.rosdep_defs.items()])
191
192    def lookup(self, rosdep_name):
193        """
194        :returns: :class:`RosdepDefinition`
195        :raises: :exc:`KeyError` If *rosdep_name* is not declared
196        """
197        return self.rosdep_defs[rosdep_name]
198
199    def keys(self):
200        """
201        :returns: list of rosdep names in this view
202        """
203        return self.rosdep_defs.keys()
204
205    def merge(self, update_entry, override=False, verbose=False):
206        """
207        Merge rosdep database update into main database.  Merge rules
208        are first entry to declare a key wins.  There are no
209        conflicts.  This rule logic is modelled after the apt sources
210        list.
211
212        :param override: Ignore first-one-wins rules and instead
213            always use rules from update_entry
214        """
215        if verbose:
216            print('view[%s]: merging from cache of [%s]' % (self.name, update_entry.origin))
217        db = self.rosdep_defs
218
219        for dep_name, dep_data in update_entry.rosdep_data.items():
220            # convert data into RosdepDefinition model
221            update_definition = RosdepDefinition(dep_name, dep_data, update_entry.origin)
222            # First rule wins or override, no rule-merging.
223            if override or dep_name not in db:
224                db[dep_name] = update_definition
225            elif dep_name in db:
226                db[dep_name].reverse_merge(dep_data, update_entry.origin, verbose=verbose)
227
228
229def prune_catkin_packages(rosdep_keys, verbose=False):
230    workspace_pkgs = catkin_packages.get_workspace_packages()
231    if not workspace_pkgs:
232        return rosdep_keys
233    for i, rosdep_key in reversed(list(enumerate(rosdep_keys))):
234        if rosdep_key in workspace_pkgs:
235            # If workspace packages listed (--catkin-workspace)
236            # and if the rosdep_key is a package in that
237            # workspace, then skip it rather than resolve it
238            if verbose:
239                print("rosdep key '{0}'".format(rosdep_key) +
240                      ' is in the catkin workspace, skipping.',
241                      file=sys.stderr)
242            del rosdep_keys[i]
243    return rosdep_keys
244
245
246def prune_skipped_packages(rosdep_keys, skipped_keys, verbose=False):
247    if not skipped_keys:
248        return rosdep_keys
249    for i, rosdep_key in reversed(list(enumerate(rosdep_keys))):
250        if rosdep_key in skipped_keys:
251            # If the key is in the list of keys to explicitly skip, skip it
252            if verbose:
253                print("rosdep key '{0}'".format(rosdep_key) +
254                      ' was listed in the skipped packages, skipping.',
255                      file=sys.stderr)
256            del rosdep_keys[i]
257    return rosdep_keys
258
259
260class RosdepLookup(object):
261    """
262    Lookup rosdep definitions.  Provides API for most
263    non-install-related commands for rosdep.
264
265    :class:`RosdepLookup` caches data as it is loaded, so changes made
266    on the filesystem will not be reflected if the rosdep information
267    has already been loaded.
268    """
269
270    def __init__(self, rosdep_db, loader):
271        """
272        :param loader: Loader to use for loading rosdep data by stack
273          name, ``RosdepLoader``
274        :param rosdep_db: Database to load definitions into, :class:`RosdepDatabase`
275        """
276        self.rosdep_db = rosdep_db
277        self.loader = loader
278
279        self._view_cache = {}  # {str: {RosdepView}}
280        self._resolve_cache = {}  # {str : (os_name, os_version, installer_key, resolution, dependencies)}
281
282        # some APIs that deal with the entire environment save errors
283        # in to self.errors instead of raising them in order to be
284        # robust to single-stack faults.
285        self.errors = []
286
287        # flag for turning on printing to console
288        self.verbose = False
289
290        self.skipped_keys = []
291
292    def get_loader(self):
293        return self.loader
294
295    def get_errors(self):
296        """
297        Retrieve error state for API calls that do not directly report
298        error state.  This is the case for APIs like
299        :meth:`RosdepLookup.where_defined` that are meant to be
300        fault-tolerant to single-stack failures.
301
302        :returns: List of exceptions, ``[Exception]``
303        """
304        return self.errors[:]
305
306    def get_rosdeps(self, resource_name, implicit=True):
307        """
308        Get rosdeps that *resource_name* (e.g. package) requires.
309
310        :param implicit: If ``True``, include implicit rosdep
311          dependencies. Default: ``True``.
312
313        :returns: list of rosdep names, ``[str]``
314        """
315        return self.loader.get_rosdeps(resource_name, implicit=implicit)
316
317    def get_resources_that_need(self, rosdep_name):
318        """
319        :param rosdep_name: name of rosdep dependency
320
321        :returns: list of package names that require rosdep, ``[str]``
322        """
323        return [k for k in self.loader.get_loadable_resources() if rosdep_name in self.get_rosdeps(k, implicit=False)]
324
325    @staticmethod
326    def create_from_rospkg(rospack=None, rosstack=None,
327                           sources_loader=None,
328                           verbose=False):
329        """
330        Create :class:`RosdepLookup` based on current ROS package
331        environment.
332
333        :param rospack: (optional) Override :class:`rospkg.RosPack`
334          instance used to crawl ROS packages.
335        :param rosstack: (optional) Override :class:`rospkg.RosStack`
336          instance used to crawl ROS stacks.
337        :param sources_loader: (optional) Override SourcesLoader used
338            for managing sources.list data sources.
339        """
340        # initialize the loader
341        if rospack is None:
342            rospack = RosPack()
343        if rosstack is None:
344            rosstack = RosStack()
345        if sources_loader is None:
346            sources_loader = SourcesListLoader.create_default(verbose=verbose)
347
348        rosdep_db = RosdepDatabase()
349
350        # Use sources list to initialize rosdep_db.  Underlay has no
351        # notion of specific resources, and its view keys are just the
352        # individual sources it can load from.  SourcesListLoader
353        # cannot do delayed evaluation of OS setting due to matcher.
354        underlay_key = SourcesListLoader.ALL_VIEW_KEY
355
356        # Create the rospkg loader on top of the underlay
357        loader = RosPkgLoader(rospack=rospack, rosstack=rosstack,
358                              underlay_key=underlay_key)
359
360        # create our actual instance
361        lookup = RosdepLookup(rosdep_db, loader)
362
363        # load in the underlay
364        lookup._load_all_views(loader=sources_loader)
365        # use dependencies to implement precedence
366        view_dependencies = sources_loader.get_loadable_views()
367        rosdep_db.set_view_data(underlay_key, {}, view_dependencies, underlay_key)
368
369        return lookup
370
371    def resolve_all(self, resources, installer_context, implicit=False):
372        """
373        Resolve all the rosdep dependencies for *resources* using *installer_context*.
374
375        :param resources: list of resources (e.g. packages), ``[str]``
376        :param installer_context: :class:`InstallerContext`
377        :param implicit: Install implicit (recursive) dependencies of
378            resources.  Default ``False``.
379
380        :returns: (resolutions, errors), ``([(str, [str])], {str: ResolutionError})``.  resolutions provides
381          an ordered list of resolution tuples.  A resolution tuple's first element is the installer
382          key (e.g.: apt or homebrew) and the second element is a list of opaque resolution values for that
383          installer. errors maps package names to an :exc:`ResolutionError` or :exc:`KeyError` exception.
384
385        :raises: :exc:`RosdepInternalError` if unexpected error in constructing dependency graph
386        :raises: :exc:`InvalidData` if a cycle occurs in constructing dependency graph
387        """
388        depend_graph = DependencyGraph()
389        errors = {}
390        # TODO: resolutions dictionary should be replaced with resolution model instead of mapping (undefined) keys.
391        for resource_name in resources:
392            try:
393                rosdep_keys = self.get_rosdeps(resource_name, implicit=implicit)
394                if self.verbose:
395                    print('resolve_all: resource [%s] requires rosdep keys [%s]' % (resource_name, ', '.join(rosdep_keys)), file=sys.stderr)
396                rosdep_keys = prune_catkin_packages(rosdep_keys, self.verbose)
397                rosdep_keys = prune_skipped_packages(rosdep_keys, self.skipped_keys, self.verbose)
398                for rosdep_key in rosdep_keys:
399                    try:
400                        installer_key, resolution, dependencies = \
401                            self.resolve(rosdep_key, resource_name, installer_context)
402                        depend_graph[rosdep_key]['installer_key'] = installer_key
403                        depend_graph[rosdep_key]['install_keys'] = list(resolution)
404                        depend_graph[rosdep_key]['dependencies'] = list(dependencies)
405                        while dependencies:
406                            depend_rosdep_key = dependencies.pop()
407                            # prevent infinite loop
408                            if depend_rosdep_key in depend_graph:
409                                continue
410                            installer_key, resolution, more_dependencies = \
411                                self.resolve(depend_rosdep_key, resource_name, installer_context)
412                            dependencies.extend(more_dependencies)
413                            depend_graph[depend_rosdep_key]['installer_key'] = installer_key
414                            depend_graph[depend_rosdep_key]['install_keys'] = list(resolution)
415                            depend_graph[depend_rosdep_key]['dependencies'] = list(more_dependencies)
416
417                    except ResolutionError as e:
418                        errors[resource_name] = e
419            except ResourceNotFound as e:
420                errors[resource_name] = e
421
422        try:
423            # TODO: I really don't like AssertionErrors here; this should be modeled as 'CyclicGraphError'
424            # or something more explicit. No need to continue if this API errors.
425            resolutions_flat = depend_graph.get_ordered_dependency_list()
426        except AssertionError as e:
427            raise InvalidData('cycle in dependency graph detected: %s' % (e))
428        except KeyError as e:
429            raise RosdepInternalError(e)
430
431        return resolutions_flat, errors
432
433    def resolve(self, rosdep_key, resource_name, installer_context):
434        """
435        Resolve a :class:`RosdepDefinition` for a particular
436        os/version spec.
437
438        :param resource_name: resource (e.g. ROS package) to resolve key within
439        :param rosdep_key: rosdep key to resolve
440        :param os_name: OS name to use for resolution
441        :param os_version: OS name to use for resolution
442
443        :returns: *(installer_key, resolution, dependencies)*, ``(str,
444          [opaque], [str])``.  *resolution* are the system
445          dependencies for the specified installer.  The value is an
446          opaque list and meant to be interpreted by the
447          installer. *dependencies* is a list of rosdep keys that the
448          definition depends on.
449
450        :raises: :exc:`ResolutionError` If *rosdep_key* cannot be resolved for *resource_name* in *installer_context*
451        :raises: :exc:`rospkg.ResourceNotFound` if *resource_name* cannot be located
452        """
453        os_name, os_version = installer_context.get_os_name_and_version()
454
455        view = self.get_rosdep_view_for_resource(resource_name)
456        if view is None:
457            raise ResolutionError(rosdep_key, None, os_name, os_version, '[%s] does not have a rosdep view' % (resource_name))
458        try:
459            # print("KEYS", view.rosdep_defs.keys())
460            definition = view.lookup(rosdep_key)
461        except KeyError:
462            rd_debug(view)
463            raise ResolutionError(rosdep_key, None, os_name, os_version, 'Cannot locate rosdep definition for [%s]' % (rosdep_key))
464
465        # check cache: the main motivation for the cache is that
466        # source rosdeps are expensive to resolve
467        if rosdep_key in self._resolve_cache:
468            cache_value = self._resolve_cache[rosdep_key]
469            cache_os_name = cache_value[0]
470            cache_os_version = cache_value[1]
471            cache_view_name = cache_value[2]
472            if (
473                cache_os_name == os_name and
474                cache_os_version == os_version and
475                cache_view_name == view.name
476            ):
477                return cache_value[3:]
478
479        # get the rosdep data for the platform
480        try:
481            installer_keys = installer_context.get_os_installer_keys(os_name)
482            default_key = installer_context.get_default_os_installer_key(os_name)
483        except KeyError:
484            raise ResolutionError(rosdep_key, definition.data, os_name, os_version, 'Unsupported OS [%s]' % (os_name))
485        installer_key, rosdep_args_dict = definition.get_rule_for_platform(os_name, os_version, installer_keys, default_key)
486
487        # resolve the rosdep data for the platform
488        try:
489            installer = installer_context.get_installer(installer_key)
490        except KeyError:
491            raise ResolutionError(rosdep_key, definition.data, os_name, os_version, 'Unsupported installer [%s]' % (installer_key))
492        resolution = installer.resolve(rosdep_args_dict)
493        dependencies = installer.get_depends(rosdep_args_dict)
494
495        # cache value
496        # the dependencies list is copied to prevent mutation before next cache hit
497        self._resolve_cache[rosdep_key] = os_name, os_version, view.name, installer_key, resolution, list(dependencies)
498
499        return installer_key, resolution, dependencies
500
501    def _load_all_views(self, loader):
502        """
503        Load all available view keys.  In general, this is equivalent
504        to loading all stacks on the package path.  If
505        :exc:`InvalidData` errors occur while loading a view,
506        they will be saved in the *errors* field.
507
508        :param loader: override self.loader
509        :raises: :exc:`RosdepInternalError`
510        """
511        for resource_name in loader.get_loadable_views():
512            try:
513                self._load_view_dependencies(resource_name, loader)
514            except ResourceNotFound as e:
515                self.errors.append(e)
516            except InvalidData as e:
517                self.errors.append(e)
518
519    def _load_view_dependencies(self, view_key, loader):
520        """
521        Initialize internal :exc:`RosdepDatabase` on demand.  Not
522        thread-safe.
523
524        :param view_key: name of view to load dependencies for.
525
526        :raises: :exc:`rospkg.ResourceNotFound` If view cannot be located
527        :raises: :exc:`InvalidData` if view's data is invaid
528        :raises: :exc:`RosdepInternalError`
529        """
530        rd_debug('_load_view_dependencies[%s]' % (view_key))
531        db = self.rosdep_db
532        if db.is_loaded(view_key):
533            return
534        try:
535            loader.load_view(view_key, db, verbose=self.verbose)
536            entry = db.get_view_data(view_key)
537            rd_debug('_load_view_dependencies[%s]: %s' % (view_key, entry.view_dependencies))
538            for d in entry.view_dependencies:
539                self._load_view_dependencies(d, loader)
540        except InvalidData:
541            # mark view as loaded: as we are caching, the valid
542            # behavior is to not attempt loading this view ever
543            # again.
544            db.mark_loaded(view_key)
545            # re-raise
546            raise
547        except KeyError as e:
548            raise RosdepInternalError(e)
549
550    def create_rosdep_view(self, view_name, view_keys, verbose=False):
551        """
552        :param view_name: name of view to create
553        :param view_keys: order list of view names to merge, first one wins
554        :param verbose: print debugging output
555        """
556        # Create view and initialize with dbs from all of the
557        # dependencies.
558        view = RosdepView(view_name)
559
560        db = self.rosdep_db
561        for view_key in view_keys:
562            db_entry = db.get_view_data(view_key)
563            view.merge(db_entry, verbose=verbose)
564        if verbose:
565            print('View [%s], merged views:\n' % (view_name) + '\n'.join([' * %s' % view_key for view_key in view_keys]), file=sys.stderr)
566        return view
567
568    def get_rosdep_view_for_resource(self, resource_name, verbose=False):
569        """
570        Get a :class:`RosdepView` for a specific ROS resource *resource_name*.
571        Views can be queries to resolve rosdep keys to
572        definitions.
573
574        :param resource_name: Name of ROS resource (e.g. stack,
575          package) to create view for, ``str``.
576
577        :returns: :class:`RosdepView` for specific ROS resource
578          *resource_name*, or ``None`` if no view is associated with this resource.
579
580        :raises: :exc:`RosdepConflict` if view cannot be created due
581          to conflict rosdep definitions.
582        :raises: :exc:`rospkg.ResourceNotFound` if *view_key* cannot be located
583        :raises: :exc:`RosdepInternalError`
584        """
585        view_key = self.loader.get_view_key(resource_name)
586        if not view_key:
587            # NOTE: this may not be the right behavior and this happens
588            # for packages that are not in a stack.
589            return None
590        return self.get_rosdep_view(view_key, verbose=verbose)
591
592    def get_rosdep_view(self, view_key, verbose=False):
593        """
594        Get a :class:`RosdepView` associated with *view_key*.  Views
595        can be queries to resolve rosdep keys to definitions.
596
597        :param view_key: Name of rosdep view (e.g. ROS stack name), ``str``
598
599        :raises: :exc:`RosdepConflict` if view cannot be created due
600          to conflict rosdep definitions.
601        :raises: :exc:`rospkg.ResourceNotFound` if *view_key* cannot be located
602        :raises: :exc:`RosdepInternalError`
603        """
604        if view_key in self._view_cache:
605            return self._view_cache[view_key]
606
607        # lazy-init
608        self._load_view_dependencies(view_key, self.loader)
609
610        # use dependencies to create view
611        try:
612            dependencies = self.rosdep_db.get_view_dependencies(view_key)
613        except KeyError as e:
614            # convert to ResourceNotFound.  This should be decoupled
615            # in the future
616            raise ResourceNotFound(str(e.args[0]))
617        # load views in order
618        view = self.create_rosdep_view(view_key, dependencies + [view_key], verbose=verbose)
619        self._view_cache[view_key] = view
620        return view
621
622    def get_views_that_define(self, rosdep_name):
623        """
624        Locate all views that directly define *rosdep_name*.  A
625        side-effect of this method is that all available rosdep files
626        in the configuration will be loaded into memory.
627
628        Error state from single-stack failures
629        (e.g. :exc:`InvalidData`, :exc:`ResourceNotFound`) are
630        not propagated.  Caller must check
631        :meth:`RosdepLookup.get_errors` to check for single-stack
632        error state.  Error state does not reset -- it accumulates.
633
634        :param rosdep_name: name of rosdep to lookup
635        :returns: list of (stack_name, origin) where rosdep is defined.
636
637        :raises: :exc:`RosdepInternalError`
638        """
639        # TODOXXX: change this to return errors object so that caller cannot ignore
640        self._load_all_views(self.loader)
641        db = self.rosdep_db
642        retval = []
643        for view_name in db.get_view_names():
644            entry = db.get_view_data(view_name)
645            # not much abstraction in the entry object
646            if rosdep_name in entry.rosdep_data:
647                retval.append((view_name, entry.origin))
648
649        return retval
650