#!/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 import aidegen_metrics from aidegen.lib import common_util from aidegen.lib import eclipse_project_file_gen from aidegen.lib import errors from aidegen.lib import ide_util from aidegen.lib import module_info from aidegen.lib import native_module_info from aidegen.lib import native_project_info from aidegen.lib import native_util from aidegen.lib import project_config from aidegen.lib import project_file_gen from aidegen.lib import project_info from aidegen.vscode import vscode_native_project_file_gen from aidegen.vscode import vscode_workspace_file_gen AIDEGEN_REPORT_LINK = ('To report the AIDEGen tool problem, please use this ' 'link: https://goto.google.com/aidegen-bug') _CONGRATULATIONS = common_util.COLORED_PASS('CONGRATULATIONS:') _LAUNCH_SUCCESS_MSG = ( 'IDE launched successfully. Please check your IDE window.') _LAUNCH_ECLIPSE_SUCCESS_MSG = ( 'The project files .classpath and .project are generated under ' '{PROJECT_PATH} and AIDEGen doesn\'t import the project automatically, ' 'please import the project manually by steps: File -> Import -> select \'' 'General\' -> \'Existing Projects into Workspace\' -> click \'Next\' -> ' 'Choose the root directory -> click \'Finish\'.') _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...].') _MAX_TIME = 1 _SKIP_BUILD_INFO_FUTURE = ''.join([ 'AIDEGen build time exceeds {} minute(s).\n'.format(_MAX_TIME), project_config.SKIP_BUILD_INFO.rstrip('.'), ' in the future.' ]) _INFO = common_util.COLORED_INFO('INFO:') _SKIP_MSG = _SKIP_BUILD_INFO_FUTURE.format( common_util.COLORED_INFO('aidegen [ module(s) ] -s')) _TIME_EXCEED_MSG = '\n{} {}\n'.format(_INFO, _SKIP_MSG) _LAUNCH_CLION_IDES = [ constant.IDE_CLION, constant.IDE_INTELLIJ, constant.IDE_ECLIPSE] _CHOOSE_LANGUAGE_MSG = ('The scope of your modules contains {} different ' 'languages as follows:\n{}\nPlease select the one you ' 'would like to implement.\t') _LANGUAGE_OPTIONS = [constant.JAVA, constant.C_CPP] 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'], # TODO(b/152571688): Show VSCode in help's Launch IDE type section at # least until one of the launching native or Java features is ready. help=('Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse, ' 'c: CLion.')) 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 jars 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.') parser.add_argument( '-e', '--exclude-paths', dest='exclude_paths', nargs='*', help='Exclude the directories in IDE.') return parser.parse_args(args) def _generate_project_files(projects): """Generate project files by IDE type. Args: projects: A list of ProjectInfo instances. """ config = project_config.ProjectConfig.get_instance() if config.ide_name == constant.IDE_ECLIPSE: eclipse_project_file_gen.EclipseConf.generate_ide_project_files( projects) else: project_file_gen.ProjectFileGenerator.generate_ide_project_files( projects) 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(project_absolute_path) if ide_util_obj.ide_name() == constant.IDE_ECLIPSE: launch_msg = ' '.join([_LAUNCH_SUCCESS_MSG, _LAUNCH_ECLIPSE_SUCCESS_MSG.format( PROJECT_PATH=project_absolute_path)]) else: launch_msg = _LAUNCH_SUCCESS_MSG print('\n{} {}\n'.format(_CONGRATULATIONS, launch_msg)) print('\n{} {}\n'.format(_INFO, _IDE_CACHE_REMINDER_MSG)) # Send the end message to Clearcut server before launching IDE to make sure # the execution time is correct. aidegen_metrics.ends_asuite_metrics(constant.EXIT_CODE_EXCEPTION) ide_util_obj.launch_ide() def _launch_native_projects(ide_util_obj, args, cmakelists): """Launches native projects with IDE. AIDEGen provides the IDE argument for CLion, but there's still a implicit way to launch it. The rules to launch it are: 1. If no target IDE, we don't have to launch any IDE for native project. 2. If the target IDE is IntelliJ or Eclipse, we should launch native projects with CLion. Args: ide_util_obj: An ide_util instance. args: An argparse.Namespace class instance holding parsed args. cmakelists: A list of CMakeLists.txt file paths. """ if not ide_util_obj: return native_ide_util_obj = ide_util_obj ide_name = constant.IDE_NAME_DICT[args.ide[0]] if ide_name in _LAUNCH_CLION_IDES: native_ide_util_obj = ide_util.get_ide_util_instance('c') if native_ide_util_obj: _launch_ide(native_ide_util_obj, ' '.join(cmakelists)) def _create_and_launch_java_projects(ide_util_obj, targets): """Launches Android of Java(Kotlin) projects with IDE. Args: ide_util_obj: An ide_util instance. targets: A list of build targets. """ projects = project_info.ProjectInfo.generate_projects(targets) project_info.ProjectInfo.multi_projects_locate_source(projects) _generate_project_files(projects) if ide_util_obj: _launch_ide(ide_util_obj, projects[0].project_absolute_path) def _get_preferred_ide_from_user(all_choices): """Provides the option list to get back users single choice. Args: all_choices: A list of string type for all options. Return: A string of the user's single choice item. """ if not all_choices: return None options = [] items = [] for index, option in enumerate(all_choices, 1): options.append('{}. {}'.format(index, option)) items.append(str(index)) query = _CHOOSE_LANGUAGE_MSG.format(len(options), '\n'.join(options)) input_data = input(query) while input_data not in items: input_data = input('Please select one.\t') return all_choices[int(input_data) - 1] # TODO(b/150578306): Refine it when new feature added. def _launch_ide_by_module_contents(args, ide_util_obj, jlist=None, clist=None, both=False): """Deals with the suitable IDE launch action. The rules AIDEGen won't ask users to choose one of the languages are: 1. Users set CLion as IDE: CLion only supports C/C++. 2. Test mode is true: if AIDEGEN_TEST_MODE is true the default language is Java. Args: args: A list of system arguments. ide_util_obj: An ide_util instance. jlist: A list of java build targets. clist: A list of native build targets. both: A boolean, True to launch both languages else False. """ if both: _launch_vscode(ide_util_obj, project_info.ProjectInfo.modules_info, jlist, clist) return if not jlist and not clist: logging.warning('\nThere is neither java nor native module needs to be' ' opened') return answer = None if constant.IDE_NAME_DICT[args.ide[0]] == constant.IDE_CLION: answer = constant.C_CPP elif common_util.to_boolean( os.environ.get(constant.AIDEGEN_TEST_MODE, 'false')): answer = constant.JAVA if not answer and jlist and clist: answer = _get_preferred_ide_from_user(_LANGUAGE_OPTIONS) if (jlist and not clist) or (answer == constant.JAVA): _create_and_launch_java_projects(ide_util_obj, jlist) return if (clist and not jlist) or (answer == constant.C_CPP): native_project_info.NativeProjectInfo.generate_projects(clist) native_project_file = native_util.generate_clion_projects(clist) if native_project_file: _launch_native_projects(ide_util_obj, args, [native_project_file]) def _launch_vscode(ide_util_obj, atest_module_info, jtargets, ctargets): """Launches targets with VSCode IDE. Args: ide_util_obj: An ide_util instance. atest_module_info: A ModuleInfo instance contains the data of module-info.json. jtargets: A list of Java project targets. ctargets: A list of native project targets. """ abs_paths = [] for target in jtargets: _, abs_path = common_util.get_related_paths(atest_module_info, target) abs_paths.append(abs_path) if ctargets: cc_module_info = native_module_info.NativeModuleInfo() native_project_info.NativeProjectInfo.generate_projects(ctargets) vs_gen = vscode_native_project_file_gen.VSCodeNativeProjectFileGenerator for target in ctargets: _, abs_path = common_util.get_related_paths(cc_module_info, target) vs_native = vs_gen(abs_path) vs_native.generate_c_cpp_properties_json_file() if abs_path not in abs_paths: abs_paths.append(abs_path) vs_path = vscode_workspace_file_gen.generate_code_workspace_file(abs_paths) if not ide_util_obj: return _launch_ide(ide_util_obj, vs_path) @common_util.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) @common_util.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. Show skip build message in aidegen main process if users command skip_build otherwise remind them to use it and include metrics supports. Args: argv: A list of system arguments. """ exit_code = constant.EXIT_CODE_NORMAL launch_ide = True try: args = _parse_args(argv) launch_ide = not args.no_launch common_util.configure_logging(args.verbose) is_whole_android_tree = project_config.is_whole_android_tree( args.targets, args.android_tree) references = [constant.ANDROID_TREE] if is_whole_android_tree else [] aidegen_metrics.starts_asuite_metrics(references) 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, errors.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 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) aidegen_metrics.ends_asuite_metrics(exit_code, traceback_str, error_message) # print out the trackback message for developers to debug print(traceback_str) raise err finally: print('\n{0} {1}\n'.format(_INFO, AIDEGEN_REPORT_LINK)) # Send the end message here on ignoring launch IDE case. if not launch_ide and exit_code is constant.EXIT_CODE_NORMAL: aidegen_metrics.ends_asuite_metrics(exit_code) def aidegen_main(args): """AIDEGen main entry. Try to generate project files for using in IDE. The process is: 1. Instantiate a ProjectConfig singleton object and initialize its environment. After creating a singleton instance for ProjectConfig, other modules can use project configurations by ProjectConfig.get_instance(). 2. Get an IDE instance from ide_util, ide_util.get_ide_util_instance will use ProjectConfig.get_instance() inside the function. 3. Setup project_info.ProjectInfo.modules_info by instantiate AidegenModuleInfo. 4. Check if projects contain native projects and launch related IDE. Args: args: A list of system arguments. """ config = project_config.ProjectConfig(args) config.init_environment() targets = config.targets # Called ide_util for pre-check the IDE existence state. ide_util_obj = ide_util.get_ide_util_instance(args.ide[0]) project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo() cc_module_info = native_module_info.NativeModuleInfo() jtargets, ctargets = native_util.get_native_and_java_projects( project_info.ProjectInfo.modules_info, cc_module_info, targets) both = config.ide_name == constant.IDE_VSCODE # Backward compatible strategy, when both java and native module exist, # check the preferred target from the user and launch single one. _launch_ide_by_module_contents(args, ide_util_obj, jtargets, ctargets, both) if __name__ == '__main__': main(sys.argv[1:])