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