| #!/usr/bin/env python3 |
| # |
| # Copyright 2018 - 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. |
| |
| """Project information.""" |
| |
| from __future__ import absolute_import |
| |
| import logging |
| import os |
| |
| from aidegen import constant |
| from aidegen.lib import common_util |
| from aidegen.lib.common_util import COLORED_INFO |
| from aidegen.lib.common_util import get_related_paths |
| |
| _KEY_ROBOTESTS = ['robotests', 'robolectric'] |
| _ANDROID_MK = 'Android.mk' |
| _ANDROID_BP = 'Android.bp' |
| _CONVERT_MK_URL = ('https://android.googlesource.com/platform/build/soong/' |
| '#convert-android_mk-files') |
| _ANDROID_MK_WARN = ( |
| '{} contains Android.mk file(s) in its dependencies:\n{}\nPlease help ' |
| 'convert these files into blueprint format in the future, otherwise ' |
| 'AIDEGen may not be able to include all module dependencies.\nPlease visit ' |
| '%s for reference on how to convert makefile.' % _CONVERT_MK_URL) |
| _ROBOLECTRIC_MODULE = 'Robolectric_all' |
| _NOT_TARGET = ('Module %s\'s class setting is %s, none of which is included in ' |
| '%s, skipping this module in the project.') |
| # The module fake-framework have the same package name with framework but empty |
| # content. It will impact the dependency for framework when referencing the |
| # package from fake-framework in IntelliJ. |
| _EXCLUDE_MODULES = ['fake-framework'] |
| |
| |
| class ProjectInfo(): |
| """Project information. |
| |
| Class attributes: |
| modules_info: A dict of all modules info by combining module-info.json |
| with module_bp_java_deps.json. |
| |
| Attributes: |
| project_absolute_path: The absolute path of the project. |
| project_relative_path: The relative path of the project to |
| constant.ANDROID_ROOT_PATH. |
| project_module_names: A list of module names under project_absolute_path |
| directory or it's subdirectories. |
| dep_modules: A dict has recursively dependent modules of |
| project_module_names. |
| git_path: The project's git path. |
| iml_path: The project's iml file path. |
| source_path: A dictionary to keep following data: |
| source_folder_path: A set contains the source folder |
| relative paths. |
| test_folder_path: A set contains the test folder relative |
| paths. |
| jar_path: A set contains the jar file paths. |
| jar_module_path: A dictionary contains the jar file and |
| the module's path mapping. |
| """ |
| |
| modules_info = {} |
| |
| def __init__(self, module_info, target=None): |
| """ProjectInfo initialize. |
| |
| Args: |
| module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| target: Includes target module or project path from user input, when |
| locating the target, project with matching module name of |
| the given target has a higher priority than project path. |
| """ |
| rel_path, abs_path = get_related_paths(module_info, target) |
| target = self._get_target_name(target, abs_path) |
| self.project_module_names = set(module_info.get_module_names(rel_path)) |
| self.project_relative_path = rel_path |
| self.project_absolute_path = abs_path |
| self.iml_path = '' |
| self._set_default_modues() |
| self._init_source_path() |
| self.dep_modules = self.get_dep_modules() |
| self._filter_out_modules() |
| self._display_convert_make_files_message(module_info, target) |
| |
| def _set_default_modues(self): |
| """Append default hard-code modules, source paths and jar files. |
| |
| 1. framework: Framework module is always needed for dependencies but it |
| might not always be located by module dependency. |
| 2. org.apache.http.legacy.stubs.system: The module can't be located |
| through module dependency. Without it, a lot of java files will have |
| error of "cannot resolve symbol" in IntelliJ since they import |
| packages android.Manifest and com.android.internal.R. |
| """ |
| # TODO(b/112058649): Do more research to clarify how to remove these |
| # hard-code sources. |
| self.project_module_names.update( |
| ['framework', 'org.apache.http.legacy.stubs.system']) |
| |
| def _init_source_path(self): |
| """Initialize source_path dictionary.""" |
| self.source_path = { |
| 'source_folder_path': set(), |
| 'test_folder_path': set(), |
| 'jar_path': set(), |
| 'jar_module_path': dict() |
| } |
| |
| def _display_convert_make_files_message(self, module_info, target): |
| """Show message info users convert their Android.mk to Android.bp. |
| |
| Args: |
| module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| target: When locating the target module or project path from users' |
| input, project with matching module name of the given target |
| has a higher priority than project path. |
| """ |
| mk_set = set(self._search_android_make_files(module_info)) |
| if mk_set: |
| print('\n{} {}\n'.format( |
| COLORED_INFO('Warning:'), |
| _ANDROID_MK_WARN.format(target, '\n'.join(mk_set)))) |
| |
| def _search_android_make_files(self, module_info): |
| """Search project and dependency modules contain Android.mk files. |
| |
| If there is only Android.mk but no Android.bp, we'll show the warning |
| message, otherwise we won't. |
| |
| Args: |
| module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| |
| Yields: |
| A string: the relative path of Android.mk. |
| """ |
| android_mk = os.path.join(self.project_absolute_path, _ANDROID_MK) |
| android_bp = os.path.join(self.project_absolute_path, _ANDROID_BP) |
| if os.path.isfile(android_mk) and not os.path.isfile(android_bp): |
| yield '\t' + os.path.join(self.project_relative_path, _ANDROID_MK) |
| for module_name in self.dep_modules: |
| rel_path, abs_path = get_related_paths(module_info, module_name) |
| mod_mk = os.path.join(abs_path, _ANDROID_MK) |
| mod_bp = os.path.join(abs_path, _ANDROID_BP) |
| if os.path.isfile(mod_mk) and not os.path.isfile(mod_bp): |
| yield '\t' + os.path.join(rel_path, _ANDROID_MK) |
| |
| def set_modules_under_project_path(self): |
| """Find modules whose class is qualified to be included under the |
| project path. |
| """ |
| logging.info('Find modules whose class is in %s under %s.', |
| common_util.TARGET_CLASSES, self.project_relative_path) |
| for name, data in self.modules_info.items(): |
| if common_util.is_project_path_relative_module( |
| data, self.project_relative_path): |
| if self._is_a_target_module(data): |
| self.project_module_names.add(name) |
| if self._is_a_robolectric_module(data): |
| self.project_module_names.add(_ROBOLECTRIC_MODULE) |
| else: |
| logging.debug(_NOT_TARGET, name, data['class'], |
| common_util.TARGET_CLASSES) |
| |
| def _filter_out_modules(self): |
| """Filter out unnecessary modules.""" |
| for module in _EXCLUDE_MODULES: |
| self.dep_modules.pop(module, None) |
| |
| @staticmethod |
| def _is_a_target_module(data): |
| """Determine if the module is a target module. |
| |
| A module's class is in {'APPS', 'JAVA_LIBRARIES', 'ROBOLECTRIC'} |
| |
| Args: |
| data: the module-info dictionary of the checked module. |
| |
| Returns: |
| A boolean, true if is a target module, otherwise false. |
| """ |
| if not 'class' in data: |
| return False |
| return any(x in data['class'] for x in common_util.TARGET_CLASSES) |
| |
| @staticmethod |
| def _is_a_robolectric_module(data): |
| """Determine if the module is a robolectric module. |
| |
| Hardcode for robotest dependency. If a folder named robotests or |
| robolectric is in the module's path hierarchy then add the module |
| Robolectric_all as a dependency. |
| |
| Args: |
| data: the module-info dictionary of the checked module. |
| |
| Returns: |
| A boolean, true if robolectric, otherwise false. |
| """ |
| if not 'path' in data: |
| return False |
| path = data['path'][0] |
| return any(key_dir in path.split(os.sep) for key_dir in _KEY_ROBOTESTS) |
| |
| def get_dep_modules(self, module_names=None, depth=0): |
| """Recursively find dependent modules of the project. |
| |
| Find dependent modules by dependencies parameter of each module. |
| For example: |
| The module_names is ['m1']. |
| The modules_info is |
| { |
| 'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']}, |
| 'm2': {'path': ['path_to_m4']}, |
| 'm3': {'path': ['path_to_m1']} |
| 'm4': {'path': []} |
| } |
| The result dependent modules are: |
| { |
| 'm1': {'dependencies': ['m2'], 'path': ['path_to_m1'] |
| 'depth': 0}, |
| 'm2': {'path': ['path_to_m4'], 'depth': 1}, |
| 'm3': {'path': ['path_to_m1'], 'depth': 0} |
| } |
| Note that: |
| 1. m4 is not in the result as it's not among dependent modules. |
| 2. m3 is in the result as it has the same path to m1. |
| |
| Args: |
| module_names: A list of module names. |
| depth: An integer shows the depth of module dependency referenced by |
| source. Zero means the max module depth. |
| |
| Returns: |
| deps: A dict contains all dependent modules data of given modules. |
| """ |
| dep = {} |
| children = set() |
| if not module_names: |
| self.set_modules_under_project_path() |
| module_names = self.project_module_names |
| self.project_module_names = set() |
| for name in module_names: |
| if (name in self.modules_info |
| and name not in self.project_module_names): |
| dep[name] = self.modules_info[name] |
| dep[name][constant.KEY_DEPTH] = depth |
| self.project_module_names.add(name) |
| if (constant.KEY_DEP in dep[name] |
| and dep[name][constant.KEY_DEP]): |
| children.update(dep[name][constant.KEY_DEP]) |
| if children: |
| dep.update(self.get_dep_modules(children, depth + 1)) |
| return dep |
| |
| @staticmethod |
| def generate_projects(module_info, targets): |
| """Generate a list of projects in one time by a list of module names. |
| |
| Args: |
| module_info: An Atest module-info instance. |
| targets: A list of target modules or project paths from user input, |
| when locating the target, project with matched module name |
| of the target has a higher priority than project path. |
| |
| Returns: |
| List: A list of ProjectInfo instances. |
| """ |
| return [ProjectInfo(module_info, target) for target in targets] |
| |
| @staticmethod |
| def _get_target_name(target, abs_path): |
| """Get target name from target's absolute path. |
| |
| If the project is for entire Android source tree, change the target to |
| source tree's root folder name. In this way, we give IDE project file |
| a more specific name. e.g, master.iml. |
| |
| Args: |
| target: Includes target module or project path from user input, when |
| locating the target, project with matching module name of |
| the given target has a higher priority than project path. |
| abs_path: A string, target's absolute path. |
| |
| Returns: |
| A string, the target name. |
| """ |
| if abs_path == constant.ANDROID_ROOT_PATH: |
| return os.path.basename(abs_path) |
| return target |