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 import errors 27from aidegen.lib import module_info 28from aidegen.lib import project_config 29from aidegen.lib import source_locator 30 31from atest import atest_utils 32 33_CONVERT_MK_URL = ('https://android.googlesource.com/platform/build/soong/' 34 '#convert-android_mk-files') 35_ANDROID_MK_WARN = ( 36 '{} contains Android.mk file(s) in its dependencies:\n{}\nPlease help ' 37 'convert these files into blueprint format in the future, otherwise ' 38 'AIDEGen may not be able to include all module dependencies.\nPlease visit ' 39 '%s for reference on how to convert makefile.' % _CONVERT_MK_URL) 40_ROBOLECTRIC_MODULE = 'Robolectric_all' 41_NOT_TARGET = ('Module %s\'s class setting is %s, none of which is included in ' 42 '%s, skipping this module in the project.') 43# The module fake-framework have the same package name with framework but empty 44# content. It will impact the dependency for framework when referencing the 45# package from fake-framework in IntelliJ. 46_EXCLUDE_MODULES = ['fake-framework'] 47# When we use atest_utils.build(), there is a command length limit on 48# soong_ui.bash. We reserve 5000 characters for rewriting the command line 49# in soong_ui.bash. 50_CMD_LENGTH_BUFFER = 5000 51# For each argument, it need a space to separate following argument. 52_BLANK_SIZE = 1 53_CORE_MODULES = [constant.FRAMEWORK_ALL, constant.CORE_ALL, 54 'org.apache.http.legacy.stubs.system'] 55 56 57class ProjectInfo: 58 """Project information. 59 60 Users should call config_project first before starting using ProjectInfo. 61 62 Class attributes: 63 modules_info: An AidegenModuleInfo instance whose name_to_module_info is 64 combining module-info.json with module_bp_java_deps.json. 65 66 Attributes: 67 project_absolute_path: The absolute path of the project. 68 project_relative_path: The relative path of the project to 69 common_util.get_android_root_dir(). 70 project_module_names: A set of module names under project_absolute_path 71 directory or it's subdirectories. 72 dep_modules: A dict has recursively dependent modules of 73 project_module_names. 74 iml_path: The project's iml file path. 75 source_path: A dictionary to keep following data: 76 source_folder_path: A set contains the source folder 77 relative paths. 78 test_folder_path: A set contains the test folder relative 79 paths. 80 jar_path: A set contains the jar file paths. 81 jar_module_path: A dictionary contains the jar file and 82 the module's path mapping, only used in 83 Eclipse. 84 r_java_path: A set contains the relative path to the 85 R.java files, only used in Eclipse. 86 srcjar_path: A source content descriptor only used in 87 IntelliJ. 88 e.g. out/.../aapt2.srcjar!/ 89 The "!/" is a content descriptor for 90 compressed files in IntelliJ. 91 is_main_project: A boolean to verify the project is main project. 92 dependencies: A list of dependency projects' iml file names, e.g. base, 93 framework-all. 94 """ 95 96 modules_info = None 97 98 def __init__(self, target=None, is_main_project=False): 99 """ProjectInfo initialize. 100 101 Args: 102 target: Includes target module or project path from user input, when 103 locating the target, project with matching module name of 104 the given target has a higher priority than project path. 105 is_main_project: A boolean, default is False. True if the target is 106 the main project, otherwise False. 107 """ 108 rel_path, abs_path = common_util.get_related_paths( 109 self.modules_info, target) 110 self.module_name = self.get_target_name(target, abs_path) 111 self.is_main_project = is_main_project 112 self.project_module_names = set( 113 self.modules_info.get_module_names(rel_path)) 114 self.project_relative_path = rel_path 115 self.project_absolute_path = abs_path 116 self.iml_path = '' 117 self._set_default_modues() 118 self._init_source_path() 119 if target == constant.FRAMEWORK_ALL: 120 self.dep_modules = self.get_dep_modules([target]) 121 else: 122 self.dep_modules = self.get_dep_modules() 123 self._filter_out_modules() 124 self._display_convert_make_files_message() 125 self.dependencies = [] 126 127 def _set_default_modues(self): 128 """Append default hard-code modules, source paths and jar files. 129 130 1. framework: Framework module is always needed for dependencies but it 131 might not always be located by module dependency. 132 2. org.apache.http.legacy.stubs.system: The module can't be located 133 through module dependency. Without it, a lot of java files will have 134 error of "cannot resolve symbol" in IntelliJ since they import 135 packages android.Manifest and com.android.internal.R. 136 """ 137 # Set the default modules framework-all and core-all as the core 138 # dependency modules. 139 self.project_module_names.update(_CORE_MODULES) 140 141 def _init_source_path(self): 142 """Initialize source_path dictionary.""" 143 self.source_path = { 144 'source_folder_path': set(), 145 'test_folder_path': set(), 146 'jar_path': set(), 147 'jar_module_path': dict(), 148 'r_java_path': set(), 149 'srcjar_path': set() 150 } 151 152 def _display_convert_make_files_message(self): 153 """Show message info users convert their Android.mk to Android.bp.""" 154 mk_set = set(self._search_android_make_files()) 155 if mk_set: 156 print('\n{} {}\n'.format( 157 common_util.COLORED_INFO('Warning:'), 158 _ANDROID_MK_WARN.format(self.module_name, '\n'.join(mk_set)))) 159 160 def _search_android_make_files(self): 161 """Search project and dependency modules contain Android.mk files. 162 163 If there is only Android.mk but no Android.bp, we'll show the warning 164 message, otherwise we won't. 165 166 Yields: 167 A string: the relative path of Android.mk. 168 """ 169 if (common_util.exist_android_mk(self.project_absolute_path) and 170 not common_util.exist_android_bp(self.project_absolute_path)): 171 yield '\t' + os.path.join(self.project_relative_path, 172 constant.ANDROID_MK) 173 for mod_name in self.dep_modules: 174 rel_path, abs_path = common_util.get_related_paths( 175 self.modules_info, mod_name) 176 if rel_path and abs_path: 177 if (common_util.exist_android_mk(abs_path) 178 and not common_util.exist_android_bp(abs_path)): 179 yield '\t' + os.path.join(rel_path, constant.ANDROID_MK) 180 181 def _get_modules_under_project_path(self, rel_path): 182 """Find modules under the rel_path. 183 184 Find modules whose class is qualified to be included as a target module. 185 186 Args: 187 rel_path: A string, the project's relative path. 188 189 Returns: 190 A set of module names. 191 """ 192 logging.info('Find modules whose class is in %s under %s.', 193 constant.TARGET_CLASSES, rel_path) 194 modules = set() 195 for name, data in self.modules_info.name_to_module_info.items(): 196 if module_info.AidegenModuleInfo.is_project_path_relative_module( 197 data, rel_path): 198 if module_info.AidegenModuleInfo.is_target_module(data): 199 modules.add(name) 200 else: 201 logging.debug(_NOT_TARGET, name, data.get('class', ''), 202 constant.TARGET_CLASSES) 203 return modules 204 205 def _get_robolectric_dep_module(self, modules): 206 """Return the robolectric module set as dependency if any module is a 207 robolectric test. 208 209 Args: 210 modules: A set of modules. 211 212 Returns: 213 A set with a robolectric_all module name if one of the modules 214 needs the robolectric test module. Otherwise return empty list. 215 """ 216 for module in modules: 217 if self.modules_info.is_robolectric_test(module): 218 return {_ROBOLECTRIC_MODULE} 219 return set() 220 221 def _filter_out_modules(self): 222 """Filter out unnecessary modules.""" 223 for module in _EXCLUDE_MODULES: 224 self.dep_modules.pop(module, None) 225 226 def get_dep_modules(self, module_names=None, depth=0): 227 """Recursively find dependent modules of the project. 228 229 Find dependent modules by dependencies parameter of each module. 230 For example: 231 The module_names is ['m1']. 232 The modules_info is 233 { 234 'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']}, 235 'm2': {'path': ['path_to_m4']}, 236 'm3': {'path': ['path_to_m1']} 237 'm4': {'path': []} 238 } 239 The result dependent modules are: 240 { 241 'm1': {'dependencies': ['m2'], 'path': ['path_to_m1'] 242 'depth': 0}, 243 'm2': {'path': ['path_to_m4'], 'depth': 1}, 244 'm3': {'path': ['path_to_m1'], 'depth': 0} 245 } 246 Note that: 247 1. m4 is not in the result as it's not among dependent modules. 248 2. m3 is in the result as it has the same path to m1. 249 250 Args: 251 module_names: A set of module names. 252 depth: An integer shows the depth of module dependency referenced by 253 source. Zero means the max module depth. 254 255 Returns: 256 deps: A dict contains all dependent modules data of given modules. 257 """ 258 dep = {} 259 children = set() 260 if not module_names: 261 module_names = self.project_module_names 262 module_names.update( 263 self._get_modules_under_project_path( 264 self.project_relative_path)) 265 module_names.update(self._get_robolectric_dep_module(module_names)) 266 self.project_module_names = set() 267 for name in module_names: 268 if (name in self.modules_info.name_to_module_info 269 and name not in self.project_module_names): 270 dep[name] = self.modules_info.name_to_module_info[name] 271 dep[name][constant.KEY_DEPTH] = depth 272 self.project_module_names.add(name) 273 if (constant.KEY_DEPENDENCIES in dep[name] 274 and dep[name][constant.KEY_DEPENDENCIES]): 275 children.update(dep[name][constant.KEY_DEPENDENCIES]) 276 if children: 277 dep.update(self.get_dep_modules(children, depth + 1)) 278 return dep 279 280 @staticmethod 281 def generate_projects(targets): 282 """Generate a list of projects in one time by a list of module names. 283 284 Args: 285 targets: A list of target modules or project paths from user input, 286 when locating the target, project with matched module name 287 of the target has a higher priority than project path. 288 289 Returns: 290 List: A list of ProjectInfo instances. 291 """ 292 return [ProjectInfo(target, i == 0) for i, target in enumerate(targets)] 293 294 @staticmethod 295 def get_target_name(target, abs_path): 296 """Get target name from target's absolute path. 297 298 If the project is for entire Android source tree, change the target to 299 source tree's root folder name. In this way, we give IDE project file 300 a more specific name. e.g, master.iml. 301 302 Args: 303 target: Includes target module or project path from user input, when 304 locating the target, project with matching module name of 305 the given target has a higher priority than project path. 306 abs_path: A string, target's absolute path. 307 308 Returns: 309 A string, the target name. 310 """ 311 if abs_path == common_util.get_android_root_dir(): 312 return os.path.basename(abs_path) 313 return target 314 315 def locate_source(self, build=True): 316 """Locate the paths of dependent source folders and jar files. 317 318 Try to reference source folder path as dependent module unless the 319 dependent module should be referenced to a jar file, such as modules 320 have jars and jarjar_rules parameter. 321 For example: 322 Module: asm-6.0 323 java_import { 324 name: 'asm-6.0', 325 host_supported: true, 326 jars: ['asm-6.0.jar'], 327 } 328 Module: bouncycastle 329 java_library { 330 name: 'bouncycastle', 331 ... 332 target: { 333 android: { 334 jarjar_rules: 'jarjar-rules.txt', 335 }, 336 }, 337 } 338 339 Args: 340 build: A boolean default to true. If false, skip building jar and 341 srcjar files, otherwise build them. 342 343 Example usage: 344 project.source_path = project.locate_source() 345 E.g. 346 project.source_path = { 347 'source_folder_path': ['path/to/source/folder1', 348 'path/to/source/folder2', ...], 349 'test_folder_path': ['path/to/test/folder', ...], 350 'jar_path': ['path/to/jar/file1', 'path/to/jar/file2', ...] 351 } 352 """ 353 if not hasattr(self, 'dep_modules') or not self.dep_modules: 354 raise errors.EmptyModuleDependencyError( 355 'Dependent modules dictionary is empty.') 356 rebuild_targets = set() 357 for module_name, module_data in self.dep_modules.items(): 358 module = self._generate_moduledata(module_name, module_data) 359 module.locate_sources_path() 360 self.source_path['source_folder_path'].update(set(module.src_dirs)) 361 self.source_path['test_folder_path'].update(set(module.test_dirs)) 362 self.source_path['r_java_path'].update(set(module.r_java_paths)) 363 self.source_path['srcjar_path'].update(set(module.srcjar_paths)) 364 self._append_jars_as_dependencies(module) 365 rebuild_targets.update(module.build_targets) 366 config = project_config.ProjectConfig.get_instance() 367 if config.is_skip_build: 368 return 369 if rebuild_targets: 370 if build: 371 batch_build_dependencies(rebuild_targets) 372 self.locate_source(build=False) 373 else: 374 logging.warning('Jar or srcjar files build skipped:\n\t%s.', 375 '\n\t'.join(rebuild_targets)) 376 377 def _generate_moduledata(self, module_name, module_data): 378 """Generate a module class to collect dependencies in IDE. 379 380 The rules of initialize a module data instance: if ide_object isn't None 381 and its ide_name is 'eclipse', we'll create an EclipseModuleData 382 instance otherwise create a ModuleData instance. 383 384 Args: 385 module_name: Name of the module. 386 module_data: A dictionary holding a module information. 387 388 Returns: 389 A ModuleData class. 390 """ 391 ide_name = project_config.ProjectConfig.get_instance().ide_name 392 if ide_name == constant.IDE_ECLIPSE: 393 return source_locator.EclipseModuleData( 394 module_name, module_data, self.project_relative_path) 395 depth = project_config.ProjectConfig.get_instance().depth 396 return source_locator.ModuleData(module_name, module_data, depth) 397 398 def _append_jars_as_dependencies(self, module): 399 """Add given module's jar files into dependent_data as dependencies. 400 401 Args: 402 module: A ModuleData instance. 403 """ 404 if module.jar_files: 405 self.source_path['jar_path'].update(module.jar_files) 406 for jar in list(module.jar_files): 407 self.source_path['jar_module_path'].update({ 408 jar: 409 module.module_path 410 }) 411 # Collecting the jar files of default core modules as dependencies. 412 if constant.KEY_DEPENDENCIES in module.module_data: 413 self.source_path['jar_path'].update([ 414 x for x in module.module_data[constant.KEY_DEPENDENCIES] 415 if common_util.is_target(x, constant.TARGET_LIBS) 416 ]) 417 418 @classmethod 419 def multi_projects_locate_source(cls, projects): 420 """Locate the paths of dependent source folders and jar files. 421 422 Args: 423 projects: A list of ProjectInfo instances. Information of a project 424 such as project relative path, project real path, project 425 dependencies. 426 """ 427 for project in projects: 428 project.locate_source() 429 430 431class MultiProjectsInfo(ProjectInfo): 432 """Multiple projects info. 433 434 Usage example: 435 project = MultiProjectsInfo(['module_name']) 436 project.collect_all_dep_modules() 437 """ 438 439 def __init__(self, targets=None): 440 """MultiProjectsInfo initialize. 441 442 Args: 443 targets: A list of module names or project paths from user's input. 444 """ 445 super().__init__(targets[0], True) 446 self._targets = targets 447 448 def collect_all_dep_modules(self): 449 """Collects all dependency modules for the projects.""" 450 self.project_module_names = set() 451 module_names = set(_CORE_MODULES) 452 for target in self._targets: 453 relpath, _ = common_util.get_related_paths(self.modules_info, 454 target) 455 module_names.update(self._get_modules_under_project_path(relpath)) 456 module_names.update(self._get_robolectric_dep_module(module_names)) 457 self.dep_modules = self.get_dep_modules(module_names) 458 459 460def batch_build_dependencies(rebuild_targets): 461 """Batch build the jar or srcjar files of the modules if they don't exist. 462 463 Command line has the max length limit, MAX_ARG_STRLEN, and 464 MAX_ARG_STRLEN = (PAGE_SIZE * 32). 465 If the build command is longer than MAX_ARG_STRLEN, this function will 466 separate the rebuild_targets into chunks with size less or equal to 467 MAX_ARG_STRLEN to make sure it can be built successfully. 468 469 Args: 470 rebuild_targets: A set of jar or srcjar files which do not exist. 471 """ 472 logging.info('Ready to build the jar or srcjar files. Files count = %s', 473 str(len(rebuild_targets))) 474 arg_max = os.sysconf('SC_PAGE_SIZE') * 32 - _CMD_LENGTH_BUFFER 475 rebuild_targets = list(rebuild_targets) 476 for start, end in iter(_separate_build_targets(rebuild_targets, arg_max)): 477 _build_target(rebuild_targets[start:end]) 478 479 480def _build_target(targets): 481 """Build the jar or srcjar files. 482 483 Use -k to keep going when some targets can't be built or build failed. 484 Use -j to speed up building. 485 486 Args: 487 targets: A list of jar or srcjar files which need to build. 488 """ 489 build_cmd = ['-k', '-j'] 490 build_cmd.extend(list(targets)) 491 verbose = True 492 if not atest_utils.build(build_cmd, verbose): 493 message = ('Build failed!\n{}\nAIDEGen will proceed but dependency ' 494 'correctness is not guaranteed if not all targets being ' 495 'built successfully.'.format('\n'.join(targets))) 496 print('\n{} {}\n'.format(common_util.COLORED_INFO('Warning:'), message)) 497 498 499def _separate_build_targets(build_targets, max_length): 500 """Separate the build_targets by limit the command size to max command 501 length. 502 503 Args: 504 build_targets: A list to be separated. 505 max_length: The max number of each build command length. 506 507 Yields: 508 The start index and end index of build_targets. 509 """ 510 arg_len = 0 511 first_item_index = 0 512 for i, item in enumerate(build_targets): 513 arg_len = arg_len + len(item) + _BLANK_SIZE 514 if arg_len > max_length: 515 yield first_item_index, i 516 first_item_index = i 517 arg_len = len(item) + _BLANK_SIZE 518 if first_item_index < len(build_targets): 519 yield first_item_index, len(build_targets) 520