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 logging 24import os 25import time 26 27from functools import partial 28from functools import wraps 29 30from aidegen import constant 31from aidegen.lib.errors import FakeModuleError 32from aidegen.lib.errors import NoModuleDefinedInModuleInfoError 33from aidegen.lib.errors import ProjectOutsideAndroidRootError 34from aidegen.lib.errors import ProjectPathNotExistError 35from atest import constants 36from atest import module_info 37from atest.atest_utils import colorize 38 39COLORED_INFO = partial(colorize, color=constants.MAGENTA, highlight=False) 40COLORED_PASS = partial(colorize, color=constants.GREEN, highlight=False) 41COLORED_FAIL = partial(colorize, color=constants.RED, highlight=False) 42FAKE_MODULE_ERROR = '{} is a fake module.' 43OUTSIDE_ROOT_ERROR = '{} is outside android root.' 44PATH_NOT_EXISTS_ERROR = 'The path {} doesn\'t exist.' 45NO_MODULE_DEFINED_ERROR = 'No modules defined at {}.' 46# Java related classes. 47JAVA_TARGET_CLASSES = ['APPS', 'JAVA_LIBRARIES', 'ROBOLECTRIC'] 48# C, C++ related classes. 49NATIVE_TARGET_CLASSES = [ 50 'HEADER_LIBRARIES', 'NATIVE_TESTS', 'STATIC_LIBRARIES', 'SHARED_LIBRARIES' 51] 52TARGET_CLASSES = JAVA_TARGET_CLASSES 53TARGET_CLASSES.extend(NATIVE_TARGET_CLASSES) 54_REBUILD_MODULE_INFO = '%s We should rebuild module-info.json file for it.' 55 56 57def time_logged(func=None, *, message='', maximum=1): 58 """Decorate a function to find out how much time it spends. 59 60 Args: 61 func: a function is to be calculated its spending time. 62 message: the message the decorated function wants to show. 63 maximum: a interger, minutes. If time exceeds the maximum time show 64 message, otherwise doesn't. 65 66 Returns: 67 The wrapper function. 68 """ 69 if func is None: 70 return partial(time_logged, message=message, maximum=maximum) 71 72 @wraps(func) 73 def wrapper(*args, **kwargs): 74 """A wrapper function.""" 75 76 start = time.time() 77 try: 78 return func(*args, **kwargs) 79 finally: 80 timestamp = time.time() - start 81 logging.debug('{}.{} takes: {:.2f}s'.format( 82 func.__module__, func.__name__, timestamp)) 83 if message and timestamp > maximum * 60: 84 print(message) 85 86 return wrapper 87 88 89def get_related_paths(atest_module_info, target=None): 90 """Get the relative and absolute paths of target from module-info. 91 92 Args: 93 atest_module_info: A ModuleInfo instance. 94 target: A string user input from command line. It could be several cases 95 such as: 96 1. Module name, e.g. Settings 97 2. Module path, e.g. packages/apps/Settings 98 3. Relative path, e.g. ../../packages/apps/Settings 99 4. Current directory, e.g. . or no argument 100 101 Return: 102 rel_path: The relative path of a module, return None if no matching 103 module found. 104 abs_path: The absolute path of a module, return None if no matching 105 module found. 106 """ 107 rel_path = None 108 abs_path = None 109 if target: 110 # User inputs a module name. 111 if atest_module_info.is_module(target): 112 paths = atest_module_info.get_paths(target) 113 if paths: 114 rel_path = paths[0] 115 abs_path = os.path.join(constant.ANDROID_ROOT_PATH, rel_path) 116 # User inputs a module path or a relative path of android root folder. 117 elif (atest_module_info.get_module_names(target) or os.path.isdir( 118 os.path.join(constant.ANDROID_ROOT_PATH, target))): 119 rel_path = target.strip(os.sep) 120 abs_path = os.path.join(constant.ANDROID_ROOT_PATH, rel_path) 121 # User inputs a relative path of current directory. 122 else: 123 abs_path = os.path.abspath(os.path.join(os.getcwd(), target)) 124 rel_path = os.path.relpath(abs_path, constant.ANDROID_ROOT_PATH) 125 else: 126 # User doesn't input. 127 abs_path = os.getcwd() 128 if is_android_root(abs_path): 129 rel_path = '' 130 else: 131 rel_path = os.path.relpath(abs_path, constant.ANDROID_ROOT_PATH) 132 return rel_path, abs_path 133 134 135def is_target_android_root(atest_module_info, targets): 136 """Check if any target is the Android root path. 137 138 Args: 139 atest_module_info: A ModuleInfo instance contains data of 140 module-info.json. 141 targets: A list of target modules or project paths from user input. 142 143 Returns: 144 True if target is Android root, otherwise False. 145 """ 146 for target in targets: 147 _, abs_path = get_related_paths(atest_module_info, target) 148 if is_android_root(abs_path): 149 return True 150 return False 151 152 153def is_android_root(abs_path): 154 """Check if an absolute path is the Android root path. 155 156 Args: 157 abs_path: The absolute path of a module. 158 159 Returns: 160 True if abs_path is Android root, otherwise False. 161 """ 162 return abs_path == constant.ANDROID_ROOT_PATH 163 164 165def has_build_target(atest_module_info, rel_path): 166 """Determine if a relative path contains buildable module. 167 168 Args: 169 atest_module_info: A ModuleInfo instance contains data of 170 module-info.json. 171 rel_path: The module path relative to android root. 172 173 Returns: 174 True if the relative path contains a build target, otherwise false. 175 """ 176 return any( 177 mod_path.startswith(rel_path) 178 for mod_path in atest_module_info.path_to_module_info) 179 180 181def _check_modules(atest_module_info, targets, raise_on_lost_module=True): 182 """Check if all targets are valid build targets. 183 184 Args: 185 atest_module_info: A ModuleInfo instance contains data of 186 module-info.json. 187 targets: A list of target modules or project paths from user input. 188 When locating the path of the target, given a matched module 189 name has priority over path. Below is the priority of locating a 190 target: 191 1. Module name, e.g. Settings 192 2. Module path, e.g. packages/apps/Settings 193 3. Relative path, e.g. ../../packages/apps/Settings 194 4. Current directory, e.g. . or no argument 195 raise_on_lost_module: A boolean, pass to _check_module to determine if 196 ProjectPathNotExistError or NoModuleDefinedInModuleInfoError 197 should be raised. 198 199 Returns: 200 True if any _check_module return flip the True/False. 201 """ 202 for target in targets: 203 if not _check_module(atest_module_info, target, raise_on_lost_module): 204 return False 205 return True 206 207 208def _check_module(atest_module_info, target, raise_on_lost_module=True): 209 """Check if a target is valid or it's a path containing build target. 210 211 Args: 212 atest_module_info: A ModuleInfo instance contains the data of 213 module-info.json. 214 target: A target module or project path from user input. 215 When locating the path of the target, given a matched module 216 name has priority over path. Below is the priority of locating a 217 target: 218 1. Module name, e.g. Settings 219 2. Module path, e.g. packages/apps/Settings 220 3. Relative path, e.g. ../../packages/apps/Settings 221 4. Current directory, e.g. . or no argument 222 raise_on_lost_module: A boolean, handles if ProjectPathNotExistError or 223 NoModuleDefinedInModuleInfoError should be raised. 224 225 Returns: 226 1. If there is no error _check_module always return True. 227 2. If there is a error, 228 a. When raise_on_lost_module is False, _check_module will raise the 229 error. 230 b. When raise_on_lost_module is True, _check_module will return 231 False if module's error is ProjectPathNotExistError or 232 NoModuleDefinedInModuleInfoError else raise the error. 233 234 Raises: 235 Raise ProjectPathNotExistError and NoModuleDefinedInModuleInfoError only 236 when raise_on_lost_module is True, others don't subject to the limit. 237 The rules for raising exceptions: 238 1. Absolute path of a module is None -> FakeModuleError 239 2. Module doesn't exist in repo root -> ProjectOutsideAndroidRootError 240 3. The given absolute path is not a dir -> ProjectPathNotExistError 241 4. If the given abs path doesn't contain any target and not repo root 242 -> NoModuleDefinedInModuleInfoError 243 """ 244 rel_path, abs_path = get_related_paths(atest_module_info, target) 245 if not abs_path: 246 err = FAKE_MODULE_ERROR.format(target) 247 logging.error(err) 248 raise FakeModuleError(err) 249 if not abs_path.startswith(constant.ANDROID_ROOT_PATH): 250 err = OUTSIDE_ROOT_ERROR.format(abs_path) 251 logging.error(err) 252 raise ProjectOutsideAndroidRootError(err) 253 if not os.path.isdir(abs_path): 254 err = PATH_NOT_EXISTS_ERROR.format(rel_path) 255 if raise_on_lost_module: 256 logging.error(err) 257 raise ProjectPathNotExistError(err) 258 logging.debug(_REBUILD_MODULE_INFO, err) 259 return False 260 if (not has_build_target(atest_module_info, rel_path) 261 and not is_android_root(abs_path)): 262 err = NO_MODULE_DEFINED_ERROR.format(rel_path) 263 if raise_on_lost_module: 264 logging.error(err) 265 raise NoModuleDefinedInModuleInfoError(err) 266 logging.debug(_REBUILD_MODULE_INFO, err) 267 return False 268 return True 269 270 271def get_abs_path(rel_path): 272 """Get absolute path from a relative path. 273 274 Args: 275 rel_path: A string, a relative path to constant.ANDROID_ROOT_PATH. 276 277 Returns: 278 abs_path: A string, an absolute path starts with 279 constant.ANDROID_ROOT_PATH. 280 """ 281 if not rel_path: 282 return constant.ANDROID_ROOT_PATH 283 if rel_path.startswith(constant.ANDROID_ROOT_PATH): 284 return rel_path 285 return os.path.join(constant.ANDROID_ROOT_PATH, rel_path) 286 287 288def is_project_path_relative_module(data, project_relative_path): 289 """Determine if the given project path is relative to the module. 290 291 The rules: 292 1. If project_relative_path is empty, it's under Android root, return 293 True. 294 2. If module's path equals or starts with project_relative_path return 295 True, otherwise return False. 296 297 Args: 298 data: the module-info dictionary of the checked module. 299 project_relative_path: project's relative path 300 301 Returns: 302 True if it's the given project path is relative to the module, otherwise 303 False. 304 """ 305 if 'path' not in data: 306 return False 307 path = data['path'][0] 308 if project_relative_path == '': 309 return True 310 if ('class' in data 311 and (path == project_relative_path 312 or path.startswith(project_relative_path + os.sep))): 313 return True 314 return False 315 316 317def is_target(src_file, src_file_extensions): 318 """Check if src_file is a type of src_file_extensions. 319 320 Args: 321 src_file: A string of the file path to be checked. 322 src_file_extensions: A list of file types to be checked 323 324 Returns: 325 True if src_file is one of the types of src_file_extensions, otherwise 326 False. 327 """ 328 return any(src_file.endswith(x) for x in src_file_extensions) 329 330 331def get_atest_module_info(targets): 332 """Get the right version of atest ModuleInfo instance. 333 334 The rules: 335 Check if the targets don't exist in ModuleInfo, we'll regain a new atest 336 ModleInfo instance by setting force_build=True and call _check_modules 337 again. If targets still don't exist, raise exceptions. 338 339 Args: 340 targets: A list of targets to be built. 341 342 Returns: 343 An atest ModuleInfo instance. 344 """ 345 amodule_info = module_info.ModuleInfo() 346 if not _check_modules(amodule_info, targets, raise_on_lost_module=False): 347 amodule_info = module_info.ModuleInfo(force_build=True) 348 _check_modules(amodule_info, targets) 349 return amodule_info 350