1# Software License Agreement (BSD License)
2#
3# Copyright (c) 2012, Willow Garage, Inc.
4# All rights reserved.
5#
6# Redistribution and use in source and binary forms, with or without
7# modification, are permitted provided that the following conditions
8# are met:
9#
10#  * Redistributions of source code must retain the above copyright
11#    notice, this list of conditions and the following disclaimer.
12#  * Redistributions in binary form must reproduce the above
13#    copyright notice, this list of conditions and the following
14#    disclaimer in the documentation and/or other materials provided
15#    with the distribution.
16#  * Neither the name of Willow Garage, Inc. nor the names of its
17#    contributors may be used to endorse or promote products derived
18#    from this software without specific prior written permission.
19#
20# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
23# FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
24# COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
25# INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
26# BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
27# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
28# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29# LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
30# ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
31# POSSIBILITY OF SUCH DAMAGE.
32
33from __future__ import print_function
34
35import os
36
37from catkin.workspace import get_source_paths, get_workspaces
38
39from catkin_pkg.packages import find_packages
40
41
42def _get_valid_search_dirs(search_dirs, project):
43    """
44    Compare param collection of search dirs with valid names, raises ValueError if invalid.
45
46    Maintains the order of param if any.
47    If project is given other names are allowed than without.
48
49    :param search_dirs: collection of foldernames (basename) to search for
50    :param project: the project to search in or None
51    :raises: ValueError
52    """
53    # define valid search folders
54    valid_global_search_dirs = ['bin', 'etc', 'include', 'lib', 'share']
55    valid_project_search_dirs = ['etc', 'include', 'libexec', 'share']
56
57    valid_search_dirs = (valid_global_search_dirs
58                         if project is None
59                         else valid_project_search_dirs)
60    if not search_dirs:
61        search_dirs = valid_search_dirs
62    else:
63        # make search folders a list
64        search_dirs = list(search_dirs)
65
66        # determine valid search folders
67        all_valid_search_dirs = set(valid_global_search_dirs).union(
68            set(valid_project_search_dirs))
69
70        # check folder name is known at all
71        diff_dirs = set(search_dirs).difference(all_valid_search_dirs)
72        if len(diff_dirs) > 0:
73            raise ValueError('Unsupported search folders: ' +
74                             ', '.join(['"%s"' % i for i in diff_dirs]))
75        # check foldername works with project arg
76        diff_dirs = set(search_dirs).difference(valid_search_dirs)
77        if len(diff_dirs) > 0:
78            msg = 'Searching %s a project can not be combined with the search folders:' % ('without' if project is None else 'for')
79            raise ValueError(msg + ', '.join(['"%s"' % i for i in diff_dirs]))
80    return search_dirs
81
82
83# OUT is always a list of folders
84#
85# IN: project=None
86# OUT: foreach ws in workspaces: foreach s in search_in: cand = ws[0] + s (+ path)
87#      add cand to result list if it exists
88#      is not defined for s == 'libexec', bailing out
89#
90# IN: project=not None
91# OUT: foreach ws in workspaces: foreach s in search_in: cand = ws[0] + s + project (+ path)
92#      except for s == 'share', cand is a list of two paths: ws[0] + s + project (+ path) and ws[1] + project (+ path)
93#      add cand to result list if it exists
94#      is not defined for s in ['bin', 'lib'], bailing out
95def find_in_workspaces(search_dirs=None, project=None, path=None, _workspaces=None, considered_paths=None, first_matching_workspace_only=False, first_match_only=False, workspace_to_source_spaces=None, source_path_to_packages=None):
96    """
97    Find all paths which match the search criteria.
98
99    All workspaces are searched in order.
100    Each workspace, each search_in subfolder, the project name and the path are concatenated to define a candidate path.
101    If the candidate path exists it is appended to the result list.
102    Note: the search might return multiple paths for 'share' from devel- and source-space.
103
104    :param search_dir: The list of subfolders to search in (default contains all valid values: 'bin', 'etc', 'lib', 'libexec', 'share'), ``list``
105    :param project: The project name to search for (optional, not possible with the global search_in folders 'bin' and 'lib'), ``str``
106    :param path: The path, ``str``
107    :param _workspaces: (optional, used for unit tests), the list of workspaces to use.
108    :param considered_paths: If not None, function will append all path that were searched
109    :param first_matching_workspace_only: if True returns all results found for first workspace with results
110    :param first_match_only: if True returns first path found (supercedes first_matching_workspace_only)
111    :param workspace_to_source_spaces: the dictionary is populated with mappings from workspaces to source paths, pass in the same dictionary to avoid repeated reading of the catkin marker file
112    :param source_path_to_packages: the dictionary is populated with mappings from source paths to packages, pass in the same dictionary to avoid repeated crawling
113    :raises ValueError: if search_dirs contains an invalid folder name
114    :returns: List of paths
115    """
116    search_dirs = _get_valid_search_dirs(search_dirs, project)
117    if 'libexec' in search_dirs:
118        search_dirs.insert(search_dirs.index('libexec'), 'lib')
119    if _workspaces is None:
120        _workspaces = get_workspaces()
121    if workspace_to_source_spaces is None:
122        workspace_to_source_spaces = {}
123    if source_path_to_packages is None:
124        source_path_to_packages = {}
125
126    paths = []
127    existing_paths = []
128    try:
129        for workspace in (_workspaces or []):
130            for sub in search_dirs:
131                # search in workspace
132                p = os.path.join(workspace, sub)
133                if project:
134                    p = os.path.join(p, project)
135                if path:
136                    p = os.path.join(p, path)
137                paths.append(p)
138                if os.path.exists(p):
139                    existing_paths.append(p)
140                    if first_match_only:
141                        raise StopIteration
142
143                # for search in share also consider source spaces
144                if project is not None and sub == 'share':
145                    if workspace not in workspace_to_source_spaces:
146                        workspace_to_source_spaces[workspace] = get_source_paths(workspace)
147                    for source_path in workspace_to_source_spaces[workspace]:
148                        if source_path not in source_path_to_packages:
149                            source_path_to_packages[source_path] = find_packages(source_path)
150                        matching_packages = [p for p, pkg in source_path_to_packages[source_path].items() if pkg.name == project]
151                        if matching_packages:
152                            p = source_path
153                            if matching_packages[0] != os.curdir:
154                                p = os.path.join(p, matching_packages[0])
155                            if path is not None:
156                                p = os.path.join(p, path)
157                            paths.append(p)
158                            if os.path.exists(p):
159                                existing_paths.append(p)
160                                if first_match_only:
161                                    raise StopIteration
162
163            if first_matching_workspace_only and existing_paths:
164                break
165
166    except StopIteration:
167        pass
168
169    if considered_paths is not None:
170        considered_paths.extend(paths)
171
172    return existing_paths
173