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"""common_util 18 19This module has a collection of functions that provide helper functions to 20other modules. 21""" 22 23import inspect 24import json 25import logging 26import os 27import re 28import sys 29import time 30import xml.dom.minidom 31 32from functools import partial 33from functools import wraps 34from xml.etree import ElementTree 35 36from aidegen import constant 37from aidegen.lib import errors 38from atest import atest_utils 39from atest import constants 40from atest import module_info 41 42 43COLORED_INFO = partial( 44 atest_utils.colorize, color=constants.MAGENTA, highlight=False) 45COLORED_PASS = partial( 46 atest_utils.colorize, color=constants.GREEN, highlight=False) 47COLORED_FAIL = partial( 48 atest_utils.colorize, color=constants.RED, highlight=False) 49FAKE_MODULE_ERROR = '{} is a fake module.' 50OUTSIDE_ROOT_ERROR = '{} is outside android root.' 51PATH_NOT_EXISTS_ERROR = 'The path {} doesn\'t exist.' 52NO_MODULE_DEFINED_ERROR = 'No modules defined at {}.' 53_REBUILD_MODULE_INFO = '%s We should rebuild module-info.json file for it.' 54_ENVSETUP_NOT_RUN = ('Please run "source build/envsetup.sh" and "lunch" before ' 55 'running aidegen.') 56_LOG_FORMAT = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s' 57_DATE_FORMAT = '%Y-%m-%d %H:%M:%S' 58_ARG_IS_NULL_ERROR = "{0}.{1}: argument '{2}' is null." 59_ARG_TYPE_INCORRECT_ERROR = "{0}.{1}: argument '{2}': type is {3}, must be {4}." 60 61 62def time_logged(func=None, *, message='', maximum=1): 63 """Decorate a function to find out how much time it spends. 64 65 Args: 66 func: a function is to be calculated its spending time. 67 message: the message the decorated function wants to show. 68 maximum: a interger, minutes. If time exceeds the maximum time show 69 message, otherwise doesn't. 70 71 Returns: 72 The wrapper function. 73 """ 74 if func is None: 75 return partial(time_logged, message=message, maximum=maximum) 76 77 @wraps(func) 78 def wrapper(*args, **kwargs): 79 """A wrapper function.""" 80 81 start = time.time() 82 try: 83 return func(*args, **kwargs) 84 finally: 85 timestamp = time.time() - start 86 logging.debug('{}.{} takes: {:.2f}s'.format( 87 func.__module__, func.__name__, timestamp)) 88 if message and timestamp > maximum * 60: 89 print(message) 90 91 return wrapper 92 93 94def get_related_paths(atest_module_info, target=None): 95 """Get the relative and absolute paths of target from module-info. 96 97 Args: 98 atest_module_info: A ModuleInfo instance. 99 target: A string user input from command line. It could be several cases 100 such as: 101 1. Module name, e.g. Settings 102 2. Module path, e.g. packages/apps/Settings 103 3. Relative path, e.g. ../../packages/apps/Settings 104 4. Current directory, e.g. '.' or no argument 105 5. An empty string, which added by AIDEGen, used for generating 106 the iml files for the whole Android repo tree. 107 e.g. 108 1. ~/aosp$ aidegen 109 2. ~/aosp/frameworks/base$ aidegen -a 110 6. An absolute path, e.g. /usr/local/home/test/aosp 111 112 Return: 113 rel_path: The relative path of a module, return None if no matching 114 module found. 115 abs_path: The absolute path of a module, return None if no matching 116 module found. 117 """ 118 rel_path = None 119 abs_path = None 120 # take the command 'aidegen .' as 'aidegen'. 121 if target == '.': 122 target = None 123 if target: 124 # For the case of whole Android repo tree. 125 if target == constant.WHOLE_ANDROID_TREE_TARGET: 126 rel_path = '' 127 abs_path = get_android_root_dir() 128 # User inputs an absolute path. 129 elif os.path.isabs(target): 130 abs_path = target 131 rel_path = os.path.relpath(abs_path, get_android_root_dir()) 132 # User inputs a module name. 133 elif atest_module_info.is_module(target): 134 paths = atest_module_info.get_paths(target) 135 if paths: 136 rel_path = paths[0].strip(os.sep) 137 abs_path = os.path.join(get_android_root_dir(), rel_path) 138 # User inputs a module path or a relative path of android root folder. 139 elif (atest_module_info.get_module_names(target) 140 or os.path.isdir(os.path.join(get_android_root_dir(), target))): 141 rel_path = target.strip(os.sep) 142 abs_path = os.path.join(get_android_root_dir(), rel_path) 143 # User inputs a relative path of current directory. 144 else: 145 abs_path = os.path.abspath(os.path.join(os.getcwd(), target)) 146 rel_path = os.path.relpath(abs_path, get_android_root_dir()) 147 else: 148 # User doesn't input. 149 abs_path = os.getcwd() 150 if is_android_root(abs_path): 151 rel_path = '' 152 else: 153 rel_path = os.path.relpath(abs_path, get_android_root_dir()) 154 return rel_path, abs_path 155 156 157def is_target_android_root(atest_module_info, targets): 158 """Check if any target is the Android root path. 159 160 Args: 161 atest_module_info: A ModuleInfo instance contains data of 162 module-info.json. 163 targets: A list of target modules or project paths from user input. 164 165 Returns: 166 True if target is Android root, otherwise False. 167 """ 168 for target in targets: 169 _, abs_path = get_related_paths(atest_module_info, target) 170 if is_android_root(abs_path): 171 return True 172 return False 173 174 175def is_android_root(abs_path): 176 """Check if an absolute path is the Android root path. 177 178 Args: 179 abs_path: The absolute path of a module. 180 181 Returns: 182 True if abs_path is Android root, otherwise False. 183 """ 184 return abs_path == get_android_root_dir() 185 186 187def has_build_target(atest_module_info, rel_path): 188 """Determine if a relative path contains buildable module. 189 190 Args: 191 atest_module_info: A ModuleInfo instance contains data of 192 module-info.json. 193 rel_path: The module path relative to android root. 194 195 Returns: 196 True if the relative path contains a build target, otherwise false. 197 """ 198 return any( 199 is_source_under_relative_path(mod_path, rel_path) 200 for mod_path in atest_module_info.path_to_module_info) 201 202 203def _check_modules(atest_module_info, targets, raise_on_lost_module=True): 204 """Check if all targets are valid build targets. 205 206 Args: 207 atest_module_info: A ModuleInfo instance contains data of 208 module-info.json. 209 targets: A list of target modules or project paths from user input. 210 When locating the path of the target, given a matched module 211 name has priority over path. Below is the priority of locating a 212 target: 213 1. Module name, e.g. Settings 214 2. Module path, e.g. packages/apps/Settings 215 3. Relative path, e.g. ../../packages/apps/Settings 216 4. Current directory, e.g. . or no argument 217 raise_on_lost_module: A boolean, pass to _check_module to determine if 218 ProjectPathNotExistError or NoModuleDefinedInModuleInfoError 219 should be raised. 220 221 Returns: 222 True if any _check_module return flip the True/False. 223 """ 224 for target in targets: 225 if not check_module(atest_module_info, target, raise_on_lost_module): 226 return False 227 return True 228 229 230def check_module(atest_module_info, target, raise_on_lost_module=True): 231 """Check if a target is valid or it's a path containing build target. 232 233 Args: 234 atest_module_info: A ModuleInfo instance contains the data of 235 module-info.json. 236 target: A target module or project path from user input. 237 When locating the path of the target, given a matched module 238 name has priority over path. Below is the priority of locating a 239 target: 240 1. Module name, e.g. Settings 241 2. Module path, e.g. packages/apps/Settings 242 3. Relative path, e.g. ../../packages/apps/Settings 243 4. Current directory, e.g. . or no argument 244 5. An absolute path, e.g. /usr/local/home/test/aosp 245 raise_on_lost_module: A boolean, handles if ProjectPathNotExistError or 246 NoModuleDefinedInModuleInfoError should be raised. 247 248 Returns: 249 1. If there is no error _check_module always return True. 250 2. If there is a error, 251 a. When raise_on_lost_module is False, _check_module will raise the 252 error. 253 b. When raise_on_lost_module is True, _check_module will return 254 False if module's error is ProjectPathNotExistError or 255 NoModuleDefinedInModuleInfoError else raise the error. 256 257 Raises: 258 Raise ProjectPathNotExistError and NoModuleDefinedInModuleInfoError only 259 when raise_on_lost_module is True, others don't subject to the limit. 260 The rules for raising exceptions: 261 1. Absolute path of a module is None -> FakeModuleError 262 2. Module doesn't exist in repo root -> ProjectOutsideAndroidRootError 263 3. The given absolute path is not a dir -> ProjectPathNotExistError 264 4. If the given abs path doesn't contain any target and not repo root 265 -> NoModuleDefinedInModuleInfoError 266 """ 267 rel_path, abs_path = get_related_paths(atest_module_info, target) 268 if not abs_path: 269 err = FAKE_MODULE_ERROR.format(target) 270 logging.error(err) 271 raise errors.FakeModuleError(err) 272 if not is_source_under_relative_path(abs_path, get_android_root_dir()): 273 err = OUTSIDE_ROOT_ERROR.format(abs_path) 274 logging.error(err) 275 raise errors.ProjectOutsideAndroidRootError(err) 276 if not os.path.isdir(abs_path): 277 err = PATH_NOT_EXISTS_ERROR.format(rel_path) 278 if raise_on_lost_module: 279 logging.error(err) 280 raise errors.ProjectPathNotExistError(err) 281 logging.debug(_REBUILD_MODULE_INFO, err) 282 return False 283 if (not has_build_target(atest_module_info, rel_path) 284 and not is_android_root(abs_path)): 285 err = NO_MODULE_DEFINED_ERROR.format(rel_path) 286 if raise_on_lost_module: 287 logging.error(err) 288 raise errors.NoModuleDefinedInModuleInfoError(err) 289 logging.debug(_REBUILD_MODULE_INFO, err) 290 return False 291 return True 292 293 294def get_abs_path(rel_path): 295 """Get absolute path from a relative path. 296 297 Args: 298 rel_path: A string, a relative path to get_android_root_dir(). 299 300 Returns: 301 abs_path: A string, an absolute path starts with 302 get_android_root_dir(). 303 """ 304 if not rel_path: 305 return get_android_root_dir() 306 if is_source_under_relative_path(rel_path, get_android_root_dir()): 307 return rel_path 308 return os.path.join(get_android_root_dir(), rel_path) 309 310 311def is_target(src_file, src_file_extensions): 312 """Check if src_file is a type of src_file_extensions. 313 314 Args: 315 src_file: A string of the file path to be checked. 316 src_file_extensions: A list of file types to be checked 317 318 Returns: 319 True if src_file is one of the types of src_file_extensions, otherwise 320 False. 321 """ 322 return any(src_file.endswith(x) for x in src_file_extensions) 323 324 325def get_atest_module_info(targets=None): 326 """Get the right version of atest ModuleInfo instance. 327 328 The rules: 329 1. If targets is None: 330 We just want to get an atest ModuleInfo instance. 331 2. If targets isn't None: 332 Check if the targets don't exist in ModuleInfo, we'll regain a new 333 atest ModleInfo instance by setting force_build=True and call 334 _check_modules again. If targets still don't exist, raise exceptions. 335 336 Args: 337 targets: A list of targets to be built. 338 339 Returns: 340 An atest ModuleInfo instance. 341 """ 342 amodule_info = module_info.ModuleInfo() 343 if targets and not _check_modules( 344 amodule_info, targets, raise_on_lost_module=False): 345 amodule_info = module_info.ModuleInfo(force_build=True) 346 _check_modules(amodule_info, targets) 347 return amodule_info 348 349 350def get_blueprint_json_path(file_name): 351 """Assemble the path of blueprint json file. 352 353 Args: 354 file_name: A string of blueprint json file name. 355 356 Returns: 357 The path of json file. 358 """ 359 return os.path.join(get_soong_out_path(), file_name) 360 361 362def back_to_cwd(func): 363 """Decorate a function change directory back to its original one. 364 365 Args: 366 func: a function is to be changed back to its original directory. 367 368 Returns: 369 The wrapper function. 370 """ 371 372 @wraps(func) 373 def wrapper(*args, **kwargs): 374 """A wrapper function.""" 375 cwd = os.getcwd() 376 try: 377 return func(*args, **kwargs) 378 finally: 379 os.chdir(cwd) 380 381 return wrapper 382 383 384def get_android_out_dir(): 385 """Get out directory in different conditions. 386 387 Returns: 388 Android out directory path. 389 """ 390 android_root_path = get_android_root_dir() 391 android_out_dir = os.getenv(constants.ANDROID_OUT_DIR) 392 out_dir_common_base = os.getenv(constant.OUT_DIR_COMMON_BASE_ENV_VAR) 393 android_out_dir_common_base = (os.path.join( 394 out_dir_common_base, os.path.basename(android_root_path)) 395 if out_dir_common_base else None) 396 return (android_out_dir or android_out_dir_common_base 397 or constant.ANDROID_DEFAULT_OUT) 398 399 400def get_android_root_dir(): 401 """Get Android root directory. 402 403 If the path doesn't exist show message to ask users to run envsetup.sh and 404 lunch first. 405 406 Returns: 407 Android root directory path. 408 """ 409 android_root_path = os.environ.get(constants.ANDROID_BUILD_TOP) 410 if not android_root_path: 411 _show_env_setup_msg_and_exit() 412 return android_root_path 413 414 415def get_aidegen_root_dir(): 416 """Get AIDEGen root directory. 417 418 Returns: 419 AIDEGen root directory path. 420 """ 421 return os.path.join(get_android_root_dir(), constant.AIDEGEN_ROOT_PATH) 422 423 424def _show_env_setup_msg_and_exit(): 425 """Show message if users didn't run envsetup.sh and lunch.""" 426 print(_ENVSETUP_NOT_RUN) 427 sys.exit(0) 428 429 430def get_soong_out_path(): 431 """Assemble out directory's soong path. 432 433 Returns: 434 Out directory's soong path. 435 """ 436 return os.path.join(get_android_root_dir(), get_android_out_dir(), 'soong') 437 438 439def configure_logging(verbose): 440 """Configure the logger. 441 442 Args: 443 verbose: A boolean. If true, display DEBUG level logs. 444 """ 445 log_format = _LOG_FORMAT 446 datefmt = _DATE_FORMAT 447 level = logging.DEBUG if verbose else logging.INFO 448 # Clear all handlers to prevent setting level not working. 449 logging.getLogger().handlers = [] 450 logging.basicConfig(level=level, format=log_format, datefmt=datefmt) 451 452 453def exist_android_bp(abs_path): 454 """Check if the Android.bp exists under specific folder. 455 456 Args: 457 abs_path: An absolute path string. 458 459 Returns: A boolean, true if the Android.bp exists under the folder, 460 otherwise false. 461 """ 462 return os.path.isfile(os.path.join(abs_path, constant.ANDROID_BP)) 463 464 465def exist_android_mk(abs_path): 466 """Check if the Android.mk exists under specific folder. 467 468 Args: 469 abs_path: An absolute path string. 470 471 Returns: A boolean, true if the Android.mk exists under the folder, 472 otherwise false. 473 """ 474 return os.path.isfile(os.path.join(abs_path, constant.ANDROID_MK)) 475 476 477def is_source_under_relative_path(source, relative_path): 478 """Check if a source file is a project relative path file. 479 480 Args: 481 source: Android source file path. 482 relative_path: Relative path of the module. 483 484 Returns: 485 True if source file is a project relative path file, otherwise False. 486 """ 487 return re.search( 488 constant.RE_INSIDE_PATH_CHECK.format(relative_path), source) 489 490 491def remove_user_home_path(data): 492 """Replace the user home path string with a constant string. 493 494 Args: 495 data: A string of xml content or an attributeError of error message. 496 497 Returns: 498 A string which replaced the user home path to $USER_HOME$. 499 """ 500 return str(data).replace(os.path.expanduser('~'), constant.USER_HOME) 501 502 503def io_error_handle(func): 504 """Decorates a function of handling io error and raising exception. 505 506 Args: 507 func: A function is to be raised exception if writing file failed. 508 509 Returns: 510 The wrapper function. 511 """ 512 513 @wraps(func) 514 def wrapper(*args, **kwargs): 515 """A wrapper function.""" 516 try: 517 return func(*args, **kwargs) 518 except (OSError, IOError) as err: 519 print('{0}.{1} I/O error: {2}'.format( 520 func.__module__, func.__name__, err)) 521 raise 522 return wrapper 523 524 525def check_args(**decls): 526 """Decorates a function to check its argument types. 527 528 Usage: 529 @check_args(name=str, text=str) 530 def parse_rule(name, text): 531 ... 532 533 Args: 534 decls: A dictionary with keys as arguments' names and values as 535 arguments' types. 536 537 Returns: 538 The wrapper function. 539 """ 540 541 def decorator(func): 542 """A wrapper function.""" 543 fmodule = func.__module__ 544 fname = func.__name__ 545 fparams = inspect.signature(func).parameters 546 547 @wraps(func) 548 def decorated(*args, **kwargs): 549 """A wrapper function.""" 550 params = dict(zip(fparams, args)) 551 for arg_name, arg_type in decls.items(): 552 try: 553 arg_val = params[arg_name] 554 except KeyError: 555 # If arg_name can't be found in function's signature, it 556 # might be a case of a partial function or default 557 # parameters, we'll neglect it. 558 if arg_name not in kwargs: 559 continue 560 arg_val = kwargs.get(arg_name) 561 if arg_val is None: 562 raise TypeError(_ARG_IS_NULL_ERROR.format( 563 fmodule, fname, arg_name)) 564 if not isinstance(arg_val, arg_type): 565 raise TypeError(_ARG_TYPE_INCORRECT_ERROR.format( 566 fmodule, fname, arg_name, type(arg_val), arg_type)) 567 return func(*args, **kwargs) 568 return decorated 569 570 return decorator 571 572 573@io_error_handle 574def dump_json_dict(json_path, data): 575 """Dumps a dictionary of data into a json file. 576 577 Args: 578 json_path: An absolute json file path string. 579 data: A dictionary of data to be written into a json file. 580 """ 581 with open(json_path, 'w') as json_file: 582 json.dump(data, json_file, indent=4) 583 584 585@io_error_handle 586def get_json_dict(json_path): 587 """Loads a json file from path and convert it into a json dictionary. 588 589 Args: 590 json_path: An absolute json file path string. 591 592 Returns: 593 A dictionary loaded from the json_path. 594 """ 595 with open(json_path) as jfile: 596 return json.load(jfile) 597 598 599@io_error_handle 600def read_file_line_to_list(file_path): 601 """Read a file line by line and write them into a list. 602 603 Args: 604 file_path: A string of a file's path. 605 606 Returns: 607 A list of the file's content by line. 608 """ 609 files = [] 610 with open(file_path, encoding='utf8') as infile: 611 for line in infile: 612 files.append(line.strip()) 613 return files 614 615 616@io_error_handle 617def read_file_content(path, encode_type='utf8'): 618 """Read file's content. 619 620 Args: 621 path: Path of input file. 622 encode_type: A string of encoding name, default to UTF-8. 623 624 Returns: 625 String: Content of the file. 626 """ 627 with open(path, encoding=encode_type) as template: 628 return template.read() 629 630 631@io_error_handle 632def file_generate(path, content): 633 """Generate file from content. 634 635 Args: 636 path: Path of target file. 637 content: String content of file. 638 """ 639 if not os.path.exists(os.path.dirname(path)): 640 os.makedirs(os.path.dirname(path)) 641 with open(path, 'w') as target: 642 target.write(content) 643 644 645def get_lunch_target(): 646 """Gets the Android lunch target in current console. 647 648 Returns: 649 A json format string of lunch target in current console. 650 """ 651 product = os.environ.get(constant.TARGET_PRODUCT) 652 build_variant = os.environ.get(constant.TARGET_BUILD_VARIANT) 653 if product and build_variant: 654 return json.dumps( 655 {constant.LUNCH_TARGET: "-".join([product, build_variant])}) 656 return None 657 658 659def get_blueprint_json_files_relative_dict(): 660 """Gets a dictionary with key: environment variable, value: absolute path. 661 662 Returns: 663 A dictionary with key: environment variable and value: absolute path of 664 the file generated by the environment varialbe. 665 """ 666 data = {} 667 root_dir = get_android_root_dir() 668 bp_java_path = os.path.join( 669 root_dir, get_blueprint_json_path( 670 constant.BLUEPRINT_JAVA_JSONFILE_NAME)) 671 data[constant.GEN_JAVA_DEPS] = bp_java_path 672 bp_cc_path = os.path.join( 673 root_dir, get_blueprint_json_path(constant.BLUEPRINT_CC_JSONFILE_NAME)) 674 data[constant.GEN_CC_DEPS] = bp_cc_path 675 data[constant.GEN_COMPDB] = os.path.join(get_soong_out_path(), 676 constant.RELATIVE_COMPDB_PATH, 677 constant.COMPDB_JSONFILE_NAME) 678 return data 679 680 681def to_pretty_xml(root, indent=" "): 682 """Gets pretty xml from an xml.etree.ElementTree root. 683 684 Args: 685 root: An element tree root. 686 indent: The indent of XML. 687 Returns: 688 A string of pretty xml. 689 """ 690 xml_string = xml.dom.minidom.parseString( 691 ElementTree.tostring(root)).toprettyxml(indent) 692 # Remove the xml declaration since IntelliJ doesn't use it. 693 xml_string = xml_string.split("\n", 1)[1] 694 # Remove the weird newline issue from toprettyxml. 695 return os.linesep.join([s for s in xml_string.splitlines() if s.strip()]) 696 697 698def to_boolean(str_bool): 699 """Converts a string to a boolean. 700 701 Args: 702 str_bool: A string in the expression of boolean type. 703 704 Returns: 705 A boolean True if the string is one of ('True', 'true', 'T', 't', '1') 706 else False. 707 """ 708 return str_bool and str_bool.lower() in ('true', 't', '1') 709 710 711def find_git_root(relpath): 712 """Finds the parent directory which has a .git folder from the relpath. 713 714 Args: 715 relpath: A string of relative path. 716 717 Returns: 718 A string of the absolute path which contains a .git, otherwise, none. 719 """ 720 dir_list = relpath.split(os.sep) 721 for i in range(len(dir_list), 0, -1): 722 real_path = os.path.join(get_android_root_dir(), 723 os.sep.join(dir_list[:i]), 724 constant.GIT_FOLDER_NAME) 725 if os.path.exists(real_path): 726 return os.path.dirname(real_path) 727 logging.warning('%s can\'t find its .git folder.', relpath) 728 return None 729