• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2#
3# Copyright 2018 - The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9#     http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17"""Project information."""
18
19from __future__ import absolute_import
20
21import logging
22import os
23
24from aidegen import constant
25from aidegen.lib import common_util
26from aidegen.lib.common_util import COLORED_INFO
27from aidegen.lib.common_util import get_related_paths
28
29_KEY_ROBOTESTS = ['robotests', 'robolectric']
30_ANDROID_MK = 'Android.mk'
31_ANDROID_BP = 'Android.bp'
32_CONVERT_MK_URL = ('https://android.googlesource.com/platform/build/soong/'
33                   '#convert-android_mk-files')
34_ANDROID_MK_WARN = (
35    '{} contains Android.mk file(s) in its dependencies:\n{}\nPlease help '
36    'convert these files into blueprint format in the future, otherwise '
37    'AIDEGen may not be able to include all module dependencies.\nPlease visit '
38    '%s for reference on how to convert makefile.' % _CONVERT_MK_URL)
39_ROBOLECTRIC_MODULE = 'Robolectric_all'
40_NOT_TARGET = ('Module %s\'s class setting is %s, none of which is included in '
41               '%s, skipping this module in the project.')
42# The module fake-framework have the same package name with framework but empty
43# content. It will impact the dependency for framework when referencing the
44# package from fake-framework in IntelliJ.
45_EXCLUDE_MODULES = ['fake-framework']
46
47
48class ProjectInfo():
49    """Project information.
50
51    Class attributes:
52        modules_info: A dict of all modules info by combining module-info.json
53                      with module_bp_java_deps.json.
54
55    Attributes:
56        project_absolute_path: The absolute path of the project.
57        project_relative_path: The relative path of the project to
58                               constant.ANDROID_ROOT_PATH.
59        project_module_names: A list of module names under project_absolute_path
60                              directory or it's subdirectories.
61        dep_modules: A dict has recursively dependent modules of
62                     project_module_names.
63        git_path: The project's git path.
64        iml_path: The project's iml file path.
65        source_path: A dictionary to keep following data:
66                     source_folder_path: A set contains the source folder
67                                         relative paths.
68                     test_folder_path: A set contains the test folder relative
69                                       paths.
70                     jar_path: A set contains the jar file paths.
71                     jar_module_path: A dictionary contains the jar file and
72                                      the module's path mapping.
73    """
74
75    modules_info = {}
76
77    def __init__(self, module_info, target=None):
78        """ProjectInfo initialize.
79
80        Args:
81            module_info: A ModuleInfo instance contains data of
82                         module-info.json.
83            target: Includes target module or project path from user input, when
84                    locating the target, project with matching module name of
85                    the given target has a higher priority than project path.
86        """
87        rel_path, abs_path = get_related_paths(module_info, target)
88        target = self._get_target_name(target, abs_path)
89        self.project_module_names = set(module_info.get_module_names(rel_path))
90        self.project_relative_path = rel_path
91        self.project_absolute_path = abs_path
92        self.iml_path = ''
93        self._set_default_modues()
94        self._init_source_path()
95        self.dep_modules = self.get_dep_modules()
96        self._filter_out_modules()
97        self._display_convert_make_files_message(module_info, target)
98
99    def _set_default_modues(self):
100        """Append default hard-code modules, source paths and jar files.
101
102        1. framework: Framework module is always needed for dependencies but it
103            might not always be located by module dependency.
104        2. org.apache.http.legacy.stubs.system: The module can't be located
105            through module dependency. Without it, a lot of java files will have
106            error of "cannot resolve symbol" in IntelliJ since they import
107            packages android.Manifest and com.android.internal.R.
108        """
109        # TODO(b/112058649): Do more research to clarify how to remove these
110        #                    hard-code sources.
111        self.project_module_names.update(
112            ['framework', 'org.apache.http.legacy.stubs.system'])
113
114    def _init_source_path(self):
115        """Initialize source_path dictionary."""
116        self.source_path = {
117            'source_folder_path': set(),
118            'test_folder_path': set(),
119            'jar_path': set(),
120            'jar_module_path': dict()
121        }
122
123    def _display_convert_make_files_message(self, module_info, target):
124        """Show message info users convert their Android.mk to Android.bp.
125
126        Args:
127            module_info: A ModuleInfo instance contains data of
128                         module-info.json.
129            target: When locating the target module or project path from users'
130                    input, project with matching module name of the given target
131                    has a higher priority than project path.
132        """
133        mk_set = set(self._search_android_make_files(module_info))
134        if mk_set:
135            print('\n{} {}\n'.format(
136                COLORED_INFO('Warning:'),
137                _ANDROID_MK_WARN.format(target, '\n'.join(mk_set))))
138
139    def _search_android_make_files(self, module_info):
140        """Search project and dependency modules contain Android.mk files.
141
142        If there is only Android.mk but no Android.bp, we'll show the warning
143        message, otherwise we won't.
144
145        Args:
146            module_info: A ModuleInfo instance contains data of
147                         module-info.json.
148
149        Yields:
150            A string: the relative path of Android.mk.
151        """
152        android_mk = os.path.join(self.project_absolute_path, _ANDROID_MK)
153        android_bp = os.path.join(self.project_absolute_path, _ANDROID_BP)
154        if os.path.isfile(android_mk) and not os.path.isfile(android_bp):
155            yield '\t' + os.path.join(self.project_relative_path, _ANDROID_MK)
156        for module_name in self.dep_modules:
157            rel_path, abs_path = get_related_paths(module_info, module_name)
158            mod_mk = os.path.join(abs_path, _ANDROID_MK)
159            mod_bp = os.path.join(abs_path, _ANDROID_BP)
160            if os.path.isfile(mod_mk) and not os.path.isfile(mod_bp):
161                yield '\t' + os.path.join(rel_path, _ANDROID_MK)
162
163    def set_modules_under_project_path(self):
164        """Find modules whose class is qualified to be included under the
165           project path.
166        """
167        logging.info('Find modules whose class is in %s under %s.',
168                     common_util.TARGET_CLASSES, self.project_relative_path)
169        for name, data in self.modules_info.items():
170            if common_util.is_project_path_relative_module(
171                    data, self.project_relative_path):
172                if self._is_a_target_module(data):
173                    self.project_module_names.add(name)
174                    if self._is_a_robolectric_module(data):
175                        self.project_module_names.add(_ROBOLECTRIC_MODULE)
176                else:
177                    logging.debug(_NOT_TARGET, name, data['class'],
178                                  common_util.TARGET_CLASSES)
179
180    def _filter_out_modules(self):
181        """Filter out unnecessary modules."""
182        for module in _EXCLUDE_MODULES:
183            self.dep_modules.pop(module, None)
184
185    @staticmethod
186    def _is_a_target_module(data):
187        """Determine if the module is a target module.
188
189        A module's class is in {'APPS', 'JAVA_LIBRARIES', 'ROBOLECTRIC'}
190
191        Args:
192            data: the module-info dictionary of the checked module.
193
194        Returns:
195            A boolean, true if is a target module, otherwise false.
196        """
197        if not 'class' in data:
198            return False
199        return any(x in data['class'] for x in common_util.TARGET_CLASSES)
200
201    @staticmethod
202    def _is_a_robolectric_module(data):
203        """Determine if the module is a robolectric module.
204
205        Hardcode for robotest dependency. If a folder named robotests or
206        robolectric is in the module's path hierarchy then add the module
207        Robolectric_all as a dependency.
208
209        Args:
210            data: the module-info dictionary of the checked module.
211
212        Returns:
213            A boolean, true if robolectric, otherwise false.
214        """
215        if not 'path' in data:
216            return False
217        path = data['path'][0]
218        return any(key_dir in path.split(os.sep) for key_dir in _KEY_ROBOTESTS)
219
220    def get_dep_modules(self, module_names=None, depth=0):
221        """Recursively find dependent modules of the project.
222
223        Find dependent modules by dependencies parameter of each module.
224        For example:
225            The module_names is ['m1'].
226            The modules_info is
227            {
228                'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']},
229                'm2': {'path': ['path_to_m4']},
230                'm3': {'path': ['path_to_m1']}
231                'm4': {'path': []}
232            }
233            The result dependent modules are:
234            {
235                'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']
236                       'depth': 0},
237                'm2': {'path': ['path_to_m4'], 'depth': 1},
238                'm3': {'path': ['path_to_m1'], 'depth': 0}
239            }
240            Note that:
241                1. m4 is not in the result as it's not among dependent modules.
242                2. m3 is in the result as it has the same path to m1.
243
244        Args:
245            module_names: A list of module names.
246            depth: An integer shows the depth of module dependency referenced by
247                   source. Zero means the max module depth.
248
249        Returns:
250            deps: A dict contains all dependent modules data of given modules.
251        """
252        dep = {}
253        children = set()
254        if not module_names:
255            self.set_modules_under_project_path()
256            module_names = self.project_module_names
257            self.project_module_names = set()
258        for name in module_names:
259            if (name in self.modules_info
260                    and name not in self.project_module_names):
261                dep[name] = self.modules_info[name]
262                dep[name][constant.KEY_DEPTH] = depth
263                self.project_module_names.add(name)
264                if (constant.KEY_DEP in dep[name]
265                        and dep[name][constant.KEY_DEP]):
266                    children.update(dep[name][constant.KEY_DEP])
267        if children:
268            dep.update(self.get_dep_modules(children, depth + 1))
269        return dep
270
271    @staticmethod
272    def generate_projects(module_info, targets):
273        """Generate a list of projects in one time by a list of module names.
274
275        Args:
276            module_info: An Atest module-info instance.
277            targets: A list of target modules or project paths from user input,
278                     when locating the target, project with matched module name
279                     of the target has a higher priority than project path.
280
281        Returns:
282            List: A list of ProjectInfo instances.
283        """
284        return [ProjectInfo(module_info, target) for target in targets]
285
286    @staticmethod
287    def _get_target_name(target, abs_path):
288        """Get target name from target's absolute path.
289
290        If the project is for entire Android source tree, change the target to
291        source tree's root folder name. In this way, we give IDE project file
292        a more specific name. e.g, master.iml.
293
294        Args:
295            target: Includes target module or project path from user input, when
296                    locating the target, project with matching module name of
297                    the given target has a higher priority than project path.
298            abs_path: A string, target's absolute path.
299
300        Returns:
301            A string, the target name.
302        """
303        if abs_path == constant.ANDROID_ROOT_PATH:
304            return os.path.basename(abs_path)
305        return target
306