#!/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
|