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