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 logging.info('\nThe batch_build_dependencies function is ' 372 'called by ProjectInfo\'s locate_source method.') 373 batch_build_dependencies(rebuild_targets) 374 self.locate_source(build=False) 375 else: 376 logging.warning('Jar or srcjar files build skipped:\n\t%s.', 377 '\n\t'.join(rebuild_targets)) 378 379 def _generate_moduledata(self, module_name, module_data): 380 """Generate a module class to collect dependencies in IDE. 381 382 The rules of initialize a module data instance: if ide_object isn't None 383 and its ide_name is 'eclipse', we'll create an EclipseModuleData 384 instance otherwise create a ModuleData instance. 385 386 Args: 387 module_name: Name of the module. 388 module_data: A dictionary holding a module information. 389 390 Returns: 391 A ModuleData class. 392 """ 393 ide_name = project_config.ProjectConfig.get_instance().ide_name 394 if ide_name == constant.IDE_ECLIPSE: 395 return source_locator.EclipseModuleData( 396 module_name, module_data, self.project_relative_path) 397 depth = project_config.ProjectConfig.get_instance().depth 398 return source_locator.ModuleData(module_name, module_data, depth) 399 400 def _append_jars_as_dependencies(self, module): 401 """Add given module's jar files into dependent_data as dependencies. 402 403 Args: 404 module: A ModuleData instance. 405 """ 406 if module.jar_files: 407 self.source_path['jar_path'].update(module.jar_files) 408 for jar in list(module.jar_files): 409 self.source_path['jar_module_path'].update({ 410 jar: 411 module.module_path 412 }) 413 # Collecting the jar files of default core modules as dependencies. 414 if constant.KEY_DEPENDENCIES in module.module_data: 415 self.source_path['jar_path'].update([ 416 x for x in module.module_data[constant.KEY_DEPENDENCIES] 417 if common_util.is_target(x, constant.TARGET_LIBS) 418 ]) 419 420 @classmethod 421 def multi_projects_locate_source(cls, projects): 422 """Locate the paths of dependent source folders and jar files. 423 424 Args: 425 projects: A list of ProjectInfo instances. Information of a project 426 such as project relative path, project real path, project 427 dependencies. 428 """ 429 for project in projects: 430 project.locate_source() 431 432 433class MultiProjectsInfo(ProjectInfo): 434 """Multiple projects info. 435 436 Usage example: 437 if folder_base: 438 project = MultiProjectsInfo(['module_name']) 439 project.collect_all_dep_modules() 440 project.gen_folder_base_dependencies() 441 else: 442 ProjectInfo.generate_projects(['module_name']) 443 444 Attributes: 445 _targets: A list of module names or project paths. 446 path_to_sources: A dictionary of modules' sources, the module's path 447 as key and the sources as value. 448 e.g. 449 { 450 'frameworks/base': { 451 'src_dirs': [], 452 'test_dirs': [], 453 'r_java_paths': [], 454 'srcjar_paths': [], 455 'jar_files': [], 456 'dep_paths': [], 457 } 458 } 459 """ 460 461 def __init__(self, targets=None): 462 """MultiProjectsInfo initialize. 463 464 Args: 465 targets: A list of module names or project paths from user's input. 466 """ 467 super().__init__(targets[0], True) 468 self._targets = targets 469 self.path_to_sources = {} 470 471 def _clear_srcjar_paths(self, module): 472 """Clears the srcjar_paths. 473 474 Args: 475 module: A ModuleData instance. 476 """ 477 module.srcjar_paths = [] 478 479 def _collect_framework_srcjar_info(self, module): 480 """Clears the framework's srcjars. 481 482 Args: 483 module: A ModuleData instance. 484 """ 485 if module.module_path == constant.FRAMEWORK_PATH: 486 framework_srcjar_path = os.path.join(constant.FRAMEWORK_PATH, 487 constant.FRAMEWORK_SRCJARS) 488 if module.module_name == constant.FRAMEWORK_ALL: 489 self.path_to_sources[framework_srcjar_path] = { 490 'src_dirs': [], 491 'test_dirs': [], 492 'r_java_paths': [], 493 'srcjar_paths': module.srcjar_paths, 494 'jar_files': [], 495 'dep_paths': [constant.FRAMEWORK_PATH], 496 } 497 # In the folder base case, AIDEGen has to ignore all module's srcjar 498 # files under the frameworks/base except the framework-all. Because 499 # there are too many duplicate srcjars of modules under the 500 # frameworks/base. So that AIDEGen keeps the srcjar files only from 501 # the framework-all module. Other modeuls' srcjar files will be 502 # removed. However, when users choose the module base case, srcjar 503 # files will be collected by the ProjectInfo class, so that the 504 # removing srcjar_paths in this class does not impact the 505 # srcjar_paths collection of modules in the ProjectInfo class. 506 self._clear_srcjar_paths(module) 507 508 def collect_all_dep_modules(self): 509 """Collects all dependency modules for the projects.""" 510 self.project_module_names.clear() 511 module_names = set(_CORE_MODULES) 512 for target in self._targets: 513 relpath, _ = common_util.get_related_paths(self.modules_info, 514 target) 515 module_names.update(self._get_modules_under_project_path(relpath)) 516 module_names.update(self._get_robolectric_dep_module(module_names)) 517 self.dep_modules = self.get_dep_modules(module_names) 518 519 def gen_folder_base_dependencies(self, module): 520 """Generates the folder base dependencies dictionary. 521 522 Args: 523 module: A ModuleData instance. 524 """ 525 mod_path = module.module_path 526 if not mod_path: 527 logging.debug('The %s\'s path is empty.', module.module_name) 528 return 529 self._collect_framework_srcjar_info(module) 530 if mod_path not in self.path_to_sources: 531 self.path_to_sources[mod_path] = { 532 'src_dirs': module.src_dirs, 533 'test_dirs': module.test_dirs, 534 'r_java_paths': module.r_java_paths, 535 'srcjar_paths': module.srcjar_paths, 536 'jar_files': module.jar_files, 537 'dep_paths': module.dep_paths, 538 } 539 else: 540 for key, val in self.path_to_sources[mod_path].items(): 541 val.extend([v for v in getattr(module, key) if v not in val]) 542 543 544def batch_build_dependencies(rebuild_targets): 545 """Batch build the jar or srcjar files of the modules if they don't exist. 546 547 Command line has the max length limit, MAX_ARG_STRLEN, and 548 MAX_ARG_STRLEN = (PAGE_SIZE * 32). 549 If the build command is longer than MAX_ARG_STRLEN, this function will 550 separate the rebuild_targets into chunks with size less or equal to 551 MAX_ARG_STRLEN to make sure it can be built successfully. 552 553 Args: 554 rebuild_targets: A set of jar or srcjar files which do not exist. 555 """ 556 logging.info('Ready to build the jar or srcjar files. Files count = %s', 557 str(len(rebuild_targets))) 558 arg_max = os.sysconf('SC_PAGE_SIZE') * 32 - _CMD_LENGTH_BUFFER 559 rebuild_targets = list(rebuild_targets) 560 for start, end in iter(_separate_build_targets(rebuild_targets, arg_max)): 561 _build_target(rebuild_targets[start:end]) 562 563 564def _build_target(targets): 565 """Build the jar or srcjar files. 566 567 Use -k to keep going when some targets can't be built or build failed. 568 Use -j to speed up building. 569 570 Args: 571 targets: A list of jar or srcjar files which need to build. 572 """ 573 build_cmd = ['-k', '-j'] 574 build_cmd.extend(list(targets)) 575 verbose = True 576 if not atest_utils.build(build_cmd, verbose): 577 message = ('Build failed!\n{}\nAIDEGen will proceed but dependency ' 578 'correctness is not guaranteed if not all targets being ' 579 'built successfully.'.format('\n'.join(targets))) 580 print('\n{} {}\n'.format(common_util.COLORED_INFO('Warning:'), message)) 581 582 583def _separate_build_targets(build_targets, max_length): 584 """Separate the build_targets by limit the command size to max command 585 length. 586 587 Args: 588 build_targets: A list to be separated. 589 max_length: The max number of each build command length. 590 591 Yields: 592 The start index and end index of build_targets. 593 """ 594 arg_len = 0 595 first_item_index = 0 596 for i, item in enumerate(build_targets): 597 arg_len = arg_len + len(item) + _BLANK_SIZE 598 if arg_len > max_length: 599 yield first_item_index, i 600 first_item_index = i 601 arg_len = len(item) + _BLANK_SIZE 602 if first_item_index < len(build_targets): 603 yield first_item_index, len(build_targets) 604