#!/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 an 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] _NO_ANY_PROJECT_EXIST = 'There is no Java, C/C++ or Rust target.' 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=['u'], help=('Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse, ' 'c: CLion, v: VS Code. The default value is \'u\': undefined.')) 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.') parser.add_argument( '-V', '--version', action='store_true', help='Print aidegen version string.') parser.add_argument( '-l', '--language', default=['u'], help=('Launch IDE with a specific language, j: Java, c: C/C++, r: ' 'Rust. The default value is \'u\': undefined.')) 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 C/C++ 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 C/C++ project. 2. If the target IDE is IntelliJ or Eclipse, we should launch C/C++ 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 _launch_ide_by_module_contents(args, ide_util_obj, language, jlist=None, clist=None, rlist=None, all_langs=False): """Deals with the suitable IDE launch action. The rules of AIDEGen launching IDE with languages are: 1. If no IDE or language is specific, the priority of the language is: a) Java aidegen frameworks/base launch Java projects of frameworks/base in IntelliJ. b) C/C++ aidegen hardware/interfaces/vibrator/aidl/default launch C/C++ project of hardware/interfaces/vibrator/aidl/default in CLion. c) Rust aidegen external/rust/crates/protobuf launch Rust project of external/rust/crates/protobuf in VS Code. 2. If the IDE is specific, launch related projects in the IDE. a) aidegen frameworks/base -i j launch Java projects of frameworks/base in IntelliJ. aidegen frameworks/base -i s launch Java projects of frameworks/base in Android Studio. aidegen frameworks/base -i e launch Java projects of frameworks/base in Eclipse. b) aidegen frameworks/base -i c launch C/C++ projects of frameworks/base in CLion. c) aidegen external/rust/crates/protobuf -i v launch Rust project of external/rust/crates/protobuf in VS Code. 3. If the launguage is specific, launch relative language projects in the relative IDE. a) aidegen frameworks/base -l j launch Java projects of frameworks/base in IntelliJ. b) aidegen frameworks/base -l c launch C/C++ projects of frameworks/base in CLion. c) aidegen external/rust/crates/protobuf -l r launch Rust projects of external/rust/crates/protobuf in VS Code. 4. Both of the IDE and language are specific, launch the IDE with the relative language projects. If the IDE conflicts with the language, the IDE is prior to the language. a) aidegen frameworks/base -i j -l j launch Java projects of frameworks/base in IntelliJ. b) aidegen frameworks/base -i s -l c launch C/C++ projects of frameworks/base in Android Studio. c) aidegen frameworks/base -i c -l j launch C/C++ projects of frameworks/base in CLion. Args: args: A list of system arguments. ide_util_obj: An ide_util instance. language: A string of the language to be edited in the IDE. jlist: A list of Java build targets. clist: A list of C/C++ build targets. rlist: A list of Rust build targets. all_langs: A boolean, True to launch all languages else False. """ if all_langs: _launch_vscode(ide_util_obj, project_info.ProjectInfo.modules_info, jlist, clist, rlist) return if not (jlist or clist or rlist): print(constant.WARN_MSG.format( common_util.COLORED_INFO('Warning:'), _NO_ANY_PROJECT_EXIST)) return if language == constant.JAVA: _create_and_launch_java_projects(ide_util_obj, jlist) return if language == 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, rtargets): """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 C/C++ project targets. rtargets: A list of Rust project targets. """ abs_paths = [] if jtargets: abs_paths.extend(_get_java_project_paths(jtargets, atest_module_info)) if ctargets: abs_paths.extend(_get_cc_project_paths(ctargets)) if rtargets: root_dir = common_util.get_android_root_dir() abs_paths.extend(_get_rust_project_paths(rtargets, root_dir)) if not (jtargets or ctargets or rtargets): print(constant.WARN_MSG.format( common_util.COLORED_INFO('Warning:'), _NO_ANY_PROJECT_EXIST)) return 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) def _get_java_project_paths(jtargets, atest_module_info): """Gets the Java absolute project paths from the input Java targets. Args: jtargets: A list of strings of Java targets. atest_module_info: A ModuleInfo instance contains the data of module-info.json. Returns: A list of the Java absolute project paths. """ abs_paths = [] for target in jtargets: _, abs_path = common_util.get_related_paths(atest_module_info, target) if abs_path: abs_paths.append(abs_path) return abs_paths def _get_cc_project_paths(ctargets): """Gets the C/C++ absolute project paths from the input C/C++ targets. Args: ctargets: A list of strings of C/C++ targets. Returns: A list of the C/C++ absolute project paths. """ abs_paths = [] 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) if not abs_path: continue 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) return abs_paths def _get_rust_project_paths(rtargets, root_dir): """Gets the Rust absolute project paths from the input Rust targets. Args: rtargets: A list of strings of Rust targets. root_dir: A string of the Android root directory. Returns: A list of the Rust absolute project paths. """ abs_paths = [] for rtarget in rtargets: path = rtarget # If rtarget is not an absolute path, make it an absolute one. if not common_util.is_source_under_relative_path(rtarget, root_dir): path = os.path.join(root_dir, rtarget) abs_paths.append(path) return abs_paths @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 ask_version = False try: args = _parse_args(argv) if args.version: ask_version = True version_file = os.path.join(os.path.dirname(__file__), constant.VERSION_FILE) print(common_util.read_file_content(version_file)) sys.exit(constant.EXIT_CODE_NORMAL) 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: if not ask_version: 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 C/C++ projects and launch related IDE. Args: args: A list of system arguments. """ config = project_config.ProjectConfig(args) config.init_environment() targets = config.targets project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo() cc_module_info = native_module_info.NativeModuleInfo() jtargets, ctargets, rtargets = native_util.get_java_cc_and_rust_projects( project_info.ProjectInfo.modules_info, cc_module_info, targets) config.language, config.ide_name = common_util.determine_language_ide( args.language[0], args.ide[0], jtargets, ctargets, rtargets) # Called ide_util for pre-check the IDE existence state. ide_util_obj = ide_util.get_ide_util_instance( constant.IDE_DICT[config.ide_name]) all_langs = config.ide_name == constant.IDE_VSCODE # Backward compatible strategy, when both java and C/C++ module exist, # check the preferred target from the user and launch single one. _launch_ide_by_module_contents(args, ide_util_obj, config.language, jtargets, ctargets, rtargets, all_langs) if __name__ == '__main__': main(sys.argv[1:])