普通文本  |  399行  |  13.15 KB

#!/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.

"""AIDEgen

This CLI generates project files for using in IntelliJ, such as:
    - iml
    - .idea/compiler.xml
    - .idea/misc.xml
    - .idea/modules.xml
    - .idea/vcs.xml
    - .idea/.name
    - .idea/copyright/Apache_2.xml
    - .idea/copyright/progiles_settings.xml

- Sample usage:
    - Change directory to AOSP root first.
    $ cd /user/home/aosp/
    - Generating project files under packages/apps/Settings folder.
    $ aidegen packages/apps/Settings
    or
    $ aidegen Settings
    or
    $ cd packages/apps/Settings;aidegen
"""

from __future__ import absolute_import

import argparse
import logging
import os
import sys
import traceback

from aidegen import constant
from aidegen.lib.android_dev_os import AndroidDevOS
from aidegen.lib import common_util
from aidegen.lib.common_util import COLORED_INFO
from aidegen.lib.common_util import COLORED_PASS
from aidegen.lib.common_util import is_android_root
from aidegen.lib.common_util import time_logged
from aidegen.lib.errors import AIDEgenError
from aidegen.lib.errors import IDENotExistError
from aidegen.lib.ide_util import IdeUtil
from aidegen.lib.aidegen_metrics import starts_asuite_metrics
from aidegen.lib.aidegen_metrics import ends_asuite_metrics
from aidegen.lib.module_info_util import generate_module_info_json
from aidegen.lib.project_file_gen import generate_eclipse_project_files
from aidegen.lib.project_file_gen import generate_ide_project_files
from aidegen.lib.project_info import ProjectInfo
from aidegen.lib.source_locator import multi_projects_locate_source

AIDEGEN_REPORT_LINK = ('To report the AIDEGen tool problem, please use this '
                       'link: https://goto.google.com/aidegen-bug')
_NO_LAUNCH_IDE_CMD = """
Can not find IDE in path: {}, you can:
    - add IDE executable to your $PATH
or  - specify the exact IDE executable path by "aidegen -p"
or  - specify "aidegen -n" to generate project file only
"""

_CONGRATULATION = COLORED_PASS('CONGRATULATION:')
_LAUNCH_SUCCESS_MSG = (
    'IDE launched successfully. Please check your IDE window.')
_IDE_CACHE_REMINDER_MSG = (
    'To prevent the existed IDE cache from impacting your IDE dependency '
    'analysis, please consider to clear IDE caches if necessary. To do that, in'
    ' IntelliJ IDEA, go to [File > Invalidate Caches / Restart...].')

_SKIP_BUILD_INFO = ('If you are sure the related modules and dependencies have '
                    'been already built, please try to use command {} to skip '
                    'the building process.')
_MAX_TIME = 1
_SKIP_BUILD_INFO_FUTURE = ''.join([
    'AIDEGen build time exceeds {} minute(s).\n'.format(_MAX_TIME),
    _SKIP_BUILD_INFO.rstrip('.'), ' in the future.'
])
_SKIP_BUILD_CMD = 'aidegen {} -s'
_INFO = COLORED_INFO('INFO:')
_SKIP_MSG = _SKIP_BUILD_INFO_FUTURE.format(
    COLORED_INFO('aidegen [ module(s) ] -s'))
_TIME_EXCEED_MSG = '\n{} {}\n'.format(_INFO, _SKIP_MSG)
_LOG_FORMAT = '%(asctime)s %(filename)s:%(lineno)s:%(levelname)s: %(message)s'
_DATE_FORMAT = '%Y-%m-%d %H:%M:%S'


def _parse_args(args):
    """Parse command line arguments.

    Args:
        args: A list of arguments.

    Returns:
        An argparse.Namespace class instance holding parsed args.
    """
    parser = argparse.ArgumentParser(
        description=__doc__,
        formatter_class=argparse.RawDescriptionHelpFormatter,
        usage=('aidegen [module_name1 module_name2... '
               'project_path1 project_path2...]'))
    parser.required = False
    parser.add_argument(
        'targets',
        type=str,
        nargs='*',
        default=[''],
        help=('Android module name or path.'
              'e.g. Settings or packages/apps/Settings'))
    parser.add_argument(
        '-d',
        '--depth',
        type=int,
        choices=range(10),
        default=0,
        help='The depth of module referenced by source.')
    parser.add_argument(
        '-v',
        '--verbose',
        action='store_true',
        help='Display DEBUG level logging.')
    parser.add_argument(
        '-i',
        '--ide',
        default=['j'],
        help='Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse.')
    parser.add_argument(
        '-p',
        '--ide-path',
        dest='ide_installed_path',
        help='IDE installed path.')
    parser.add_argument(
        '-n', '--no_launch', action='store_true', help='Do not launch IDE.')
    parser.add_argument(
        '-r',
        '--config-reset',
        dest='config_reset',
        action='store_true',
        help='Reset all saved configurations, e.g., preferred IDE version.')
    parser.add_argument(
        '-s',
        '--skip-build',
        dest='skip_build',
        action='store_true',
        help=('Skip building jar or modules that create java files in build '
              'time, e.g. R/AIDL/Logtags.'))
    parser.add_argument(
        '-a',
        '--android-tree',
        dest='android_tree',
        action='store_true',
        help='Generate whole Android source tree project file for IDE.')
    return parser.parse_args(args)


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)


def _get_ide_util_instance(args):
    """Get an IdeUtil class instance for launching IDE.

    Args:
        args: A list of arguments.

    Returns:
        A IdeUtil class instance.
    """
    if args.no_launch:
        return None
    ide_util_obj = IdeUtil(args.ide_installed_path, args.ide[0],
                           args.config_reset,
                           AndroidDevOS.MAC == AndroidDevOS.get_os_type())
    if not ide_util_obj.is_ide_installed():
        ipath = args.ide_installed_path or ide_util_obj.get_default_path()
        err = _NO_LAUNCH_IDE_CMD.format(ipath)
        logging.error(err)
        raise IDENotExistError(err)
    return ide_util_obj


def _check_skip_build(args):
    """Check if users skip building target, display the warning message.

    Args:
        args: A list of arguments.
    """
    if not args.skip_build:
        msg = _SKIP_BUILD_INFO.format(
            COLORED_INFO(_SKIP_BUILD_CMD.format(' '.join(args.targets))))
        print('\n{} {}\n'.format(_INFO, msg))


def _generate_project_files(ide, projects):
    """Generate project files by IDE type.

    Args:
        ide: A character to represent IDE type.
        projects: A list of ProjectInfo instances.
    """
    if ide.lower() == 'e':
        generate_eclipse_project_files(projects)
    else:
        generate_ide_project_files(projects)


def _compile_targets_for_whole_android_tree(atest_module_info, targets, cwd):
    """Compile a list of targets to include whole Android tree in the project.

    Adding the whole Android tree to the project will do two things,
    1. If current working directory is not Android root, change the target to
       its relative path to root and change current working directory to root.
       If we don't change directory it's hard to deal with the whole Android
       tree together with the sub-project.
    2. If the whole Android tree target is not in the target list, insert it to
       the first one.

    Args:
        atest_module_info: A instance of atest module-info object.
        targets: A list of targets to be built.
        cwd: A path of current working directory.

    Returns:
        A list of targets after adjustment.
    """
    new_targets = []
    if is_android_root(cwd):
        new_targets = list(targets)
    else:
        for target in targets:
            _, abs_path = common_util.get_related_paths(atest_module_info,
                                                        target)
            rel_path = os.path.relpath(abs_path, constant.ANDROID_ROOT_PATH)
            new_targets.append(rel_path)
        os.chdir(constant.ANDROID_ROOT_PATH)

    if new_targets[0] != '':
        new_targets.insert(0, '')
    return new_targets


def _launch_ide(ide_util_obj, project_absolute_path):
    """Launch IDE through ide_util instance.

    To launch IDE,
    1. Set IDE config.
    2. For IntelliJ, use .idea as open target is better than .iml file,
       because open the latter is like to open a kind of normal file.
    3. Show _LAUNCH_SUCCESS_MSG to remind users IDE being launched.

    Args:
        ide_util_obj: An ide_util instance.
        project_absolute_path: A string of project absolute path.
    """
    ide_util_obj.config_ide()
    ide_util_obj.launch_ide(project_absolute_path)
    print('\n{} {}\n'.format(_CONGRATULATION, _LAUNCH_SUCCESS_MSG))


def _check_whole_android_tree(atest_module_info, targets, android_tree):
    """Check if it's a building project file for the whole Android tree.

    The rules:
    1. If users command aidegen under Android root, e.g.,
       root$ aidegen
       that implies users would like to launch the whole Android tree, AIDEGen
       should set the flag android_tree True.
    2. If android_tree is True, add whole Android tree to the project.

    Args:
        atest_module_info: A instance of atest module-info object.
        targets: A list of targets to be built.
        android_tree: A boolean, True if it's a whole Android tree case,
                      otherwise False.

    Returns:
        A list of targets to be built.
    """
    cwd = os.getcwd()
    if not android_tree and is_android_root(cwd) and targets == ['']:
        android_tree = True
    new_targets = targets
    if android_tree:
        new_targets = _compile_targets_for_whole_android_tree(
            atest_module_info, targets, cwd)
    return new_targets


@time_logged(message=_TIME_EXCEED_MSG, maximum=_MAX_TIME)
def main_with_message(args):
    """Main entry with skip build message.

    Args:
        args: A list of system arguments.
    """
    aidegen_main(args)


@time_logged
def main_without_message(args):
    """Main entry without skip build message.

    Args:
        args: A list of system arguments.
    """
    aidegen_main(args)


# pylint: disable=broad-except
def main(argv):
    """Main entry.

    Try to generates project files for using in IDE.

    Args:
        argv: A list of system arguments.
    """
    exit_code = constant.EXIT_CODE_NORMAL
    try:
        args = _parse_args(argv)
        _configure_logging(args.verbose)
        starts_asuite_metrics()
        if args.skip_build:
            main_without_message(args)
        else:
            main_with_message(args)
    except BaseException as err:
        exit_code = constant.EXIT_CODE_EXCEPTION
        _, exc_value, exc_traceback = sys.exc_info()
        if isinstance(err, AIDEgenError):
            exit_code = constant.EXIT_CODE_AIDEGEN_EXCEPTION
        # Filter out sys.Exit(0) case, which is not an exception case.
        if isinstance(err, SystemExit) and exc_value.code == 0:
            exit_code = constant.EXIT_CODE_NORMAL
    finally:
        if exit_code is not constant.EXIT_CODE_NORMAL:
            error_message = str(exc_value)
            traceback_list = traceback.format_tb(exc_traceback)
            traceback_list.append(error_message)
            traceback_str = ''.join(traceback_list)
            # print out the trackback message for developers to debug
            print(traceback_str)
            ends_asuite_metrics(exit_code, traceback_str, error_message)
        else:
            ends_asuite_metrics(exit_code)


def aidegen_main(args):
    """AIDEGen main entry.

    Try to generates project files for using in IDE.

    Args:
        args: A list of system arguments.
    """
    # Pre-check for IDE relevant case, then handle dependency graph job.
    ide_util_obj = _get_ide_util_instance(args)
    _check_skip_build(args)
    atest_module_info = common_util.get_atest_module_info(args.targets)
    targets = _check_whole_android_tree(atest_module_info, args.targets,
                                        args.android_tree)
    ProjectInfo.modules_info = generate_module_info_json(
        atest_module_info, targets, args.verbose, args.skip_build)
    projects = ProjectInfo.generate_projects(atest_module_info, targets)
    multi_projects_locate_source(projects, args.verbose, args.depth,
                                 constant.IDE_NAME_DICT[args.ide[0]],
                                 args.skip_build)
    _generate_project_files(args.ide[0], projects)
    if ide_util_obj:
        _launch_ide(ide_util_obj, projects[0].project_absolute_path)


if __name__ == '__main__':
    try:
        main(sys.argv[1:])
    finally:
        print('\n{0} {1}\n\n{0} {2}\n'.format(_INFO, AIDEGEN_REPORT_LINK,
                                              _IDE_CACHE_REMINDER_MSG))