| #!/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. |
| |
| """common_util |
| |
| This module has a collection of functions that provide helper functions to |
| other modules. |
| """ |
| |
| import logging |
| import os |
| import sys |
| import time |
| |
| from functools import partial |
| from functools import wraps |
| |
| from aidegen import constant |
| from aidegen.lib import errors |
| from atest import atest_utils |
| from atest import constants |
| from atest import module_info |
| |
| |
| COLORED_INFO = partial( |
| atest_utils.colorize, color=constants.MAGENTA, highlight=False) |
| COLORED_PASS = partial( |
| atest_utils.colorize, color=constants.GREEN, highlight=False) |
| COLORED_FAIL = partial( |
| atest_utils.colorize, color=constants.RED, highlight=False) |
| FAKE_MODULE_ERROR = '{} is a fake module.' |
| OUTSIDE_ROOT_ERROR = '{} is outside android root.' |
| PATH_NOT_EXISTS_ERROR = 'The path {} doesn\'t exist.' |
| NO_MODULE_DEFINED_ERROR = 'No modules defined at {}.' |
| _REBUILD_MODULE_INFO = '%s We should rebuild module-info.json file for it.' |
| _ENVSETUP_NOT_RUN = ('Please run "source build/envsetup.sh" and "lunch" before ' |
| 'running aidegen.') |
| _LOG_FORMAT = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s' |
| _DATE_FORMAT = '%Y-%m-%d %H:%M:%S' |
| |
| |
| def time_logged(func=None, *, message='', maximum=1): |
| """Decorate a function to find out how much time it spends. |
| |
| Args: |
| func: a function is to be calculated its spending time. |
| message: the message the decorated function wants to show. |
| maximum: a interger, minutes. If time exceeds the maximum time show |
| message, otherwise doesn't. |
| |
| Returns: |
| The wrapper function. |
| """ |
| if func is None: |
| return partial(time_logged, message=message, maximum=maximum) |
| |
| @wraps(func) |
| def wrapper(*args, **kwargs): |
| """A wrapper function.""" |
| |
| start = time.time() |
| try: |
| return func(*args, **kwargs) |
| finally: |
| timestamp = time.time() - start |
| logging.debug('{}.{} takes: {:.2f}s'.format( |
| func.__module__, func.__name__, timestamp)) |
| if message and timestamp > maximum * 60: |
| print(message) |
| |
| return wrapper |
| |
| |
| def get_related_paths(atest_module_info, target=None): |
| """Get the relative and absolute paths of target from module-info. |
| |
| Args: |
| atest_module_info: A ModuleInfo instance. |
| target: A string user input from command line. It could be several cases |
| such as: |
| 1. Module name, e.g. Settings |
| 2. Module path, e.g. packages/apps/Settings |
| 3. Relative path, e.g. ../../packages/apps/Settings |
| 4. Current directory, e.g. . or no argument |
| |
| Return: |
| rel_path: The relative path of a module, return None if no matching |
| module found. |
| abs_path: The absolute path of a module, return None if no matching |
| module found. |
| """ |
| rel_path = None |
| abs_path = None |
| if target: |
| # User inputs a module name. |
| if atest_module_info.is_module(target): |
| paths = atest_module_info.get_paths(target) |
| if paths: |
| rel_path = paths[0] |
| abs_path = os.path.join(get_android_root_dir(), rel_path) |
| # User inputs a module path or a relative path of android root folder. |
| elif (atest_module_info.get_module_names(target) |
| or os.path.isdir(os.path.join(get_android_root_dir(), target))): |
| rel_path = target.strip(os.sep) |
| abs_path = os.path.join(get_android_root_dir(), rel_path) |
| # User inputs a relative path of current directory. |
| else: |
| abs_path = os.path.abspath(os.path.join(os.getcwd(), target)) |
| rel_path = os.path.relpath(abs_path, get_android_root_dir()) |
| else: |
| # User doesn't input. |
| abs_path = os.getcwd() |
| if is_android_root(abs_path): |
| rel_path = '' |
| else: |
| rel_path = os.path.relpath(abs_path, get_android_root_dir()) |
| return rel_path, abs_path |
| |
| |
| def is_target_android_root(atest_module_info, targets): |
| """Check if any target is the Android root path. |
| |
| Args: |
| atest_module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| targets: A list of target modules or project paths from user input. |
| |
| Returns: |
| True if target is Android root, otherwise False. |
| """ |
| for target in targets: |
| _, abs_path = get_related_paths(atest_module_info, target) |
| if is_android_root(abs_path): |
| return True |
| return False |
| |
| |
| def is_android_root(abs_path): |
| """Check if an absolute path is the Android root path. |
| |
| Args: |
| abs_path: The absolute path of a module. |
| |
| Returns: |
| True if abs_path is Android root, otherwise False. |
| """ |
| return abs_path == get_android_root_dir() |
| |
| |
| def has_build_target(atest_module_info, rel_path): |
| """Determine if a relative path contains buildable module. |
| |
| Args: |
| atest_module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| rel_path: The module path relative to android root. |
| |
| Returns: |
| True if the relative path contains a build target, otherwise false. |
| """ |
| return any( |
| mod_path.startswith(rel_path) |
| for mod_path in atest_module_info.path_to_module_info) |
| |
| |
| def _check_modules(atest_module_info, targets, raise_on_lost_module=True): |
| """Check if all targets are valid build targets. |
| |
| Args: |
| atest_module_info: A ModuleInfo instance contains data of |
| module-info.json. |
| targets: A list of target modules or project paths from user input. |
| When locating the path of the target, given a matched module |
| name has priority over path. Below is the priority of locating a |
| target: |
| 1. Module name, e.g. Settings |
| 2. Module path, e.g. packages/apps/Settings |
| 3. Relative path, e.g. ../../packages/apps/Settings |
| 4. Current directory, e.g. . or no argument |
| raise_on_lost_module: A boolean, pass to _check_module to determine if |
| ProjectPathNotExistError or NoModuleDefinedInModuleInfoError |
| should be raised. |
| |
| Returns: |
| True if any _check_module return flip the True/False. |
| """ |
| for target in targets: |
| if not _check_module(atest_module_info, target, raise_on_lost_module): |
| return False |
| return True |
| |
| |
| def _check_module(atest_module_info, target, raise_on_lost_module=True): |
| """Check if a target is valid or it's a path containing build target. |
| |
| Args: |
| atest_module_info: A ModuleInfo instance contains the data of |
| module-info.json. |
| target: A target module or project path from user input. |
| When locating the path of the target, given a matched module |
| name has priority over path. Below is the priority of locating a |
| target: |
| 1. Module name, e.g. Settings |
| 2. Module path, e.g. packages/apps/Settings |
| 3. Relative path, e.g. ../../packages/apps/Settings |
| 4. Current directory, e.g. . or no argument |
| raise_on_lost_module: A boolean, handles if ProjectPathNotExistError or |
| NoModuleDefinedInModuleInfoError should be raised. |
| |
| Returns: |
| 1. If there is no error _check_module always return True. |
| 2. If there is a error, |
| a. When raise_on_lost_module is False, _check_module will raise the |
| error. |
| b. When raise_on_lost_module is True, _check_module will return |
| False if module's error is ProjectPathNotExistError or |
| NoModuleDefinedInModuleInfoError else raise the error. |
| |
| Raises: |
| Raise ProjectPathNotExistError and NoModuleDefinedInModuleInfoError only |
| when raise_on_lost_module is True, others don't subject to the limit. |
| The rules for raising exceptions: |
| 1. Absolute path of a module is None -> FakeModuleError |
| 2. Module doesn't exist in repo root -> ProjectOutsideAndroidRootError |
| 3. The given absolute path is not a dir -> ProjectPathNotExistError |
| 4. If the given abs path doesn't contain any target and not repo root |
| -> NoModuleDefinedInModuleInfoError |
| """ |
| rel_path, abs_path = get_related_paths(atest_module_info, target) |
| if not abs_path: |
| err = FAKE_MODULE_ERROR.format(target) |
| logging.error(err) |
| raise errors.FakeModuleError(err) |
| if not abs_path.startswith(get_android_root_dir()): |
| err = OUTSIDE_ROOT_ERROR.format(abs_path) |
| logging.error(err) |
| raise errors.ProjectOutsideAndroidRootError(err) |
| if not os.path.isdir(abs_path): |
| err = PATH_NOT_EXISTS_ERROR.format(rel_path) |
| if raise_on_lost_module: |
| logging.error(err) |
| raise errors.ProjectPathNotExistError(err) |
| logging.debug(_REBUILD_MODULE_INFO, err) |
| return False |
| if (not has_build_target(atest_module_info, rel_path) |
| and not is_android_root(abs_path)): |
| err = NO_MODULE_DEFINED_ERROR.format(rel_path) |
| if raise_on_lost_module: |
| logging.error(err) |
| raise errors.NoModuleDefinedInModuleInfoError(err) |
| logging.debug(_REBUILD_MODULE_INFO, err) |
| return False |
| return True |
| |
| |
| def get_abs_path(rel_path): |
| """Get absolute path from a relative path. |
| |
| Args: |
| rel_path: A string, a relative path to get_android_root_dir(). |
| |
| Returns: |
| abs_path: A string, an absolute path starts with |
| get_android_root_dir(). |
| """ |
| if not rel_path: |
| return get_android_root_dir() |
| if rel_path.startswith(get_android_root_dir()): |
| return rel_path |
| return os.path.join(get_android_root_dir(), rel_path) |
| |
| |
| def is_project_path_relative_module(data, project_relative_path): |
| """Determine if the given project path is relative to the module. |
| |
| The rules: |
| 1. If project_relative_path is empty, it's under Android root, return |
| True. |
| 2. If module's path equals or starts with project_relative_path return |
| True, otherwise return False. |
| |
| Args: |
| data: the module-info dictionary of the checked module. |
| project_relative_path: project's relative path |
| |
| Returns: |
| True if it's the given project path is relative to the module, otherwise |
| False. |
| """ |
| if 'path' not in data: |
| return False |
| path = data['path'][0] |
| if project_relative_path == '': |
| return True |
| if ('class' in data |
| and (path == project_relative_path |
| or path.startswith(project_relative_path + os.sep))): |
| return True |
| return False |
| |
| |
| def is_target(src_file, src_file_extensions): |
| """Check if src_file is a type of src_file_extensions. |
| |
| Args: |
| src_file: A string of the file path to be checked. |
| src_file_extensions: A list of file types to be checked |
| |
| Returns: |
| True if src_file is one of the types of src_file_extensions, otherwise |
| False. |
| """ |
| return any(src_file.endswith(x) for x in src_file_extensions) |
| |
| |
| def get_atest_module_info(targets=None): |
| """Get the right version of atest ModuleInfo instance. |
| |
| The rules: |
| 1. If targets is None: |
| We just want to get an atest ModuleInfo instance. |
| 2. If targets isn't None: |
| Check if the targets don't exist in ModuleInfo, we'll regain a new |
| atest ModleInfo instance by setting force_build=True and call |
| _check_modules again. If targets still don't exist, raise exceptions. |
| |
| Args: |
| targets: A list of targets to be built. |
| |
| Returns: |
| An atest ModuleInfo instance. |
| """ |
| amodule_info = module_info.ModuleInfo() |
| if targets and not _check_modules( |
| amodule_info, targets, raise_on_lost_module=False): |
| amodule_info = module_info.ModuleInfo(force_build=True) |
| _check_modules(amodule_info, targets) |
| return amodule_info |
| |
| |
| def read_file_content(path): |
| """Read file's content. |
| |
| Args: |
| path: Path of input file. |
| |
| Returns: |
| String: Content of the file. |
| """ |
| with open(path) as template: |
| return template.read() |
| |
| |
| def file_generate(path, content): |
| """Generate file from content. |
| |
| Args: |
| path: Path of target file. |
| content: String content of file. |
| """ |
| if not os.path.exists(os.path.dirname(path)): |
| os.makedirs(os.path.dirname(path)) |
| with open(path, 'w') as target: |
| target.write(content) |
| |
| |
| def get_blueprint_json_path(): |
| """Assemble the path of blueprint json file. |
| |
| Returns: |
| module_bp_java_deps.json path. |
| """ |
| return os.path.join(get_soong_out_path(), constant.BLUEPRINT_JSONFILE_NAME) |
| |
| |
| def back_to_cwd(func): |
| """Decorate a function change directory back to its original one. |
| |
| Args: |
| func: a function is to be changed back to its original directory. |
| |
| Returns: |
| The wrapper function. |
| """ |
| |
| @wraps(func) |
| def wrapper(*args, **kwargs): |
| """A wrapper function.""" |
| cwd = os.getcwd() |
| try: |
| return func(*args, **kwargs) |
| finally: |
| os.chdir(cwd) |
| |
| return wrapper |
| |
| |
| def get_android_out_dir(): |
| """Get out directory in different conditions. |
| |
| Returns: |
| Android out directory path. |
| """ |
| android_root_path = get_android_root_dir() |
| android_out_dir = os.environ.get(constants.ANDROID_OUT_DIR) |
| out_dir_common_base = os.getenv(constant.OUT_DIR_COMMON_BASE_ENV_VAR) |
| android_out_dir_common_base = (os.path.join( |
| out_dir_common_base, os.path.basename(android_root_path)) |
| if out_dir_common_base else None) |
| return (android_out_dir or android_out_dir_common_base |
| or constant.ANDROID_DEFAULT_OUT) |
| |
| |
| def get_android_root_dir(): |
| """Get Android root directory. |
| |
| If the path doesn't exist show message to ask users to run envsetup.sh and |
| lunch first. |
| |
| Returns: |
| Android root directory path. |
| """ |
| android_root_path = os.environ.get(constants.ANDROID_BUILD_TOP) |
| if not android_root_path: |
| _show_env_setup_msg_and_exit() |
| return android_root_path |
| |
| |
| def get_aidegen_root_dir(): |
| """Get AIDEGen root directory. |
| |
| Returns: |
| AIDEGen root directory path. |
| """ |
| return os.path.join(get_android_root_dir(), constant.AIDEGEN_ROOT_PATH) |
| |
| |
| def _show_env_setup_msg_and_exit(): |
| """Show message if users didn't run envsetup.sh and lunch.""" |
| print(_ENVSETUP_NOT_RUN) |
| sys.exit(0) |
| |
| |
| def get_soong_out_path(): |
| """Assemble out directory's soong path. |
| |
| Returns: |
| Out directory's soong path. |
| """ |
| return os.path.join(get_android_root_dir(), get_android_out_dir(), 'soong') |
| |
| |
| def configure_logging(verbose): |
| """Configure the logger. |
| |
| Args: |
| verbose: A boolean. If true, display DEBUG level logs. |
| """ |
| log_format = _LOG_FORMAT |
| datefmt = _DATE_FORMAT |
| level = logging.DEBUG if verbose else logging.INFO |
| logging.basicConfig(level=level, format=log_format, datefmt=datefmt) |