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"""AIDEgen 18 19This CLI generates project files for using in IntelliJ, such as: 20 - iml 21 - .idea/compiler.xml 22 - .idea/misc.xml 23 - .idea/modules.xml 24 - .idea/vcs.xml 25 - .idea/.name 26 - .idea/copyright/Apache_2.xml 27 - .idea/copyright/progiles_settings.xml 28 29- Sample usage: 30 - Change directory to AOSP root first. 31 $ cd /user/home/aosp/ 32 - Generating project files under packages/apps/Settings folder. 33 $ aidegen packages/apps/Settings 34 or 35 $ aidegen Settings 36 or 37 $ cd packages/apps/Settings;aidegen 38""" 39 40from __future__ import absolute_import 41 42import argparse 43import logging 44import os 45import sys 46import traceback 47 48from aidegen import constant 49from aidegen.lib import aidegen_metrics 50from aidegen.lib import common_util 51from aidegen.lib import eclipse_project_file_gen 52from aidegen.lib import errors 53from aidegen.lib import ide_util 54from aidegen.lib import module_info 55from aidegen.lib import native_module_info 56from aidegen.lib import native_project_info 57from aidegen.lib import native_util 58from aidegen.lib import project_config 59from aidegen.lib import project_file_gen 60from aidegen.lib import project_info 61from aidegen.vscode import vscode_native_project_file_gen 62from aidegen.vscode import vscode_workspace_file_gen 63 64AIDEGEN_REPORT_LINK = ('To report the AIDEGen tool problem, please use this ' 65 'link: https://goto.google.com/aidegen-bug') 66_CONGRATULATIONS = common_util.COLORED_PASS('CONGRATULATIONS:') 67_LAUNCH_SUCCESS_MSG = ( 68 'IDE launched successfully. Please check your IDE window.') 69_LAUNCH_ECLIPSE_SUCCESS_MSG = ( 70 'The project files .classpath and .project are generated under ' 71 '{PROJECT_PATH} and AIDEGen doesn\'t import the project automatically, ' 72 'please import the project manually by steps: File -> Import -> select \'' 73 'General\' -> \'Existing Projects into Workspace\' -> click \'Next\' -> ' 74 'Choose the root directory -> click \'Finish\'.') 75_IDE_CACHE_REMINDER_MSG = ( 76 'To prevent the existed IDE cache from impacting your IDE dependency ' 77 'analysis, please consider to clear IDE caches if necessary. To do that, in' 78 ' IntelliJ IDEA, go to [File > Invalidate Caches / Restart...].') 79 80_MAX_TIME = 1 81_SKIP_BUILD_INFO_FUTURE = ''.join([ 82 'AIDEGen build time exceeds {} minute(s).\n'.format(_MAX_TIME), 83 project_config.SKIP_BUILD_INFO.rstrip('.'), ' in the future.' 84]) 85_INFO = common_util.COLORED_INFO('INFO:') 86_SKIP_MSG = _SKIP_BUILD_INFO_FUTURE.format( 87 common_util.COLORED_INFO('aidegen [ module(s) ] -s')) 88_TIME_EXCEED_MSG = '\n{} {}\n'.format(_INFO, _SKIP_MSG) 89_LAUNCH_CLION_IDES = [ 90 constant.IDE_CLION, constant.IDE_INTELLIJ, constant.IDE_ECLIPSE] 91_CHOOSE_LANGUAGE_MSG = ('The scope of your modules contains {} different ' 92 'languages as follows:\n{}\nPlease select the one you ' 93 'would like to implement.\t') 94_LANGUAGE_OPTIONS = [constant.JAVA, constant.C_CPP] 95 96 97def _parse_args(args): 98 """Parse command line arguments. 99 100 Args: 101 args: A list of arguments. 102 103 Returns: 104 An argparse.Namespace class instance holding parsed args. 105 """ 106 parser = argparse.ArgumentParser( 107 description=__doc__, 108 formatter_class=argparse.RawDescriptionHelpFormatter, 109 usage=('aidegen [module_name1 module_name2... ' 110 'project_path1 project_path2...]')) 111 parser.required = False 112 parser.add_argument( 113 'targets', 114 type=str, 115 nargs='*', 116 default=[''], 117 help=('Android module name or path.' 118 'e.g. Settings or packages/apps/Settings')) 119 parser.add_argument( 120 '-d', 121 '--depth', 122 type=int, 123 choices=range(10), 124 default=0, 125 help='The depth of module referenced by source.') 126 parser.add_argument( 127 '-v', 128 '--verbose', 129 action='store_true', 130 help='Display DEBUG level logging.') 131 parser.add_argument( 132 '-i', 133 '--ide', 134 default=['j'], 135 # TODO(b/152571688): Show VSCode in help's Launch IDE type section at 136 # least until one of the launching native or Java features is ready. 137 help=('Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse, ' 138 'c: CLion.')) 139 parser.add_argument( 140 '-p', 141 '--ide-path', 142 dest='ide_installed_path', 143 help='IDE installed path.') 144 parser.add_argument( 145 '-n', '--no_launch', action='store_true', help='Do not launch IDE.') 146 parser.add_argument( 147 '-r', 148 '--config-reset', 149 dest='config_reset', 150 action='store_true', 151 help='Reset all saved configurations, e.g., preferred IDE version.') 152 parser.add_argument( 153 '-s', 154 '--skip-build', 155 dest='skip_build', 156 action='store_true', 157 help=('Skip building jars or modules that create java files in build ' 158 'time, e.g. R/AIDL/Logtags.')) 159 parser.add_argument( 160 '-a', 161 '--android-tree', 162 dest='android_tree', 163 action='store_true', 164 help='Generate whole Android source tree project file for IDE.') 165 parser.add_argument( 166 '-e', 167 '--exclude-paths', 168 dest='exclude_paths', 169 nargs='*', 170 help='Exclude the directories in IDE.') 171 return parser.parse_args(args) 172 173 174def _generate_project_files(projects): 175 """Generate project files by IDE type. 176 177 Args: 178 projects: A list of ProjectInfo instances. 179 """ 180 config = project_config.ProjectConfig.get_instance() 181 if config.ide_name == constant.IDE_ECLIPSE: 182 eclipse_project_file_gen.EclipseConf.generate_ide_project_files( 183 projects) 184 else: 185 project_file_gen.ProjectFileGenerator.generate_ide_project_files( 186 projects) 187 188 189def _launch_ide(ide_util_obj, project_absolute_path): 190 """Launch IDE through ide_util instance. 191 192 To launch IDE, 193 1. Set IDE config. 194 2. For IntelliJ, use .idea as open target is better than .iml file, 195 because open the latter is like to open a kind of normal file. 196 3. Show _LAUNCH_SUCCESS_MSG to remind users IDE being launched. 197 198 Args: 199 ide_util_obj: An ide_util instance. 200 project_absolute_path: A string of project absolute path. 201 """ 202 ide_util_obj.config_ide(project_absolute_path) 203 if ide_util_obj.ide_name() == constant.IDE_ECLIPSE: 204 launch_msg = ' '.join([_LAUNCH_SUCCESS_MSG, 205 _LAUNCH_ECLIPSE_SUCCESS_MSG.format( 206 PROJECT_PATH=project_absolute_path)]) 207 else: 208 launch_msg = _LAUNCH_SUCCESS_MSG 209 print('\n{} {}\n'.format(_CONGRATULATIONS, launch_msg)) 210 print('\n{} {}\n'.format(_INFO, _IDE_CACHE_REMINDER_MSG)) 211 # Send the end message to Clearcut server before launching IDE to make sure 212 # the execution time is correct. 213 aidegen_metrics.ends_asuite_metrics(constant.EXIT_CODE_EXCEPTION) 214 ide_util_obj.launch_ide() 215 216 217def _launch_native_projects(ide_util_obj, args, cmakelists): 218 """Launches native projects with IDE. 219 220 AIDEGen provides the IDE argument for CLion, but there's still a implicit 221 way to launch it. The rules to launch it are: 222 1. If no target IDE, we don't have to launch any IDE for native project. 223 2. If the target IDE is IntelliJ or Eclipse, we should launch native 224 projects with CLion. 225 226 Args: 227 ide_util_obj: An ide_util instance. 228 args: An argparse.Namespace class instance holding parsed args. 229 cmakelists: A list of CMakeLists.txt file paths. 230 """ 231 if not ide_util_obj: 232 return 233 native_ide_util_obj = ide_util_obj 234 ide_name = constant.IDE_NAME_DICT[args.ide[0]] 235 if ide_name in _LAUNCH_CLION_IDES: 236 native_ide_util_obj = ide_util.get_ide_util_instance('c') 237 if native_ide_util_obj: 238 _launch_ide(native_ide_util_obj, ' '.join(cmakelists)) 239 240 241def _create_and_launch_java_projects(ide_util_obj, targets): 242 """Launches Android of Java(Kotlin) projects with IDE. 243 244 Args: 245 ide_util_obj: An ide_util instance. 246 targets: A list of build targets. 247 """ 248 projects = project_info.ProjectInfo.generate_projects(targets) 249 project_info.ProjectInfo.multi_projects_locate_source(projects) 250 _generate_project_files(projects) 251 if ide_util_obj: 252 _launch_ide(ide_util_obj, projects[0].project_absolute_path) 253 254 255def _get_preferred_ide_from_user(all_choices): 256 """Provides the option list to get back users single choice. 257 258 Args: 259 all_choices: A list of string type for all options. 260 261 Return: 262 A string of the user's single choice item. 263 """ 264 if not all_choices: 265 return None 266 options = [] 267 items = [] 268 for index, option in enumerate(all_choices, 1): 269 options.append('{}. {}'.format(index, option)) 270 items.append(str(index)) 271 query = _CHOOSE_LANGUAGE_MSG.format(len(options), '\n'.join(options)) 272 input_data = input(query) 273 while input_data not in items: 274 input_data = input('Please select one.\t') 275 return all_choices[int(input_data) - 1] 276 277 278# TODO(b/150578306): Refine it when new feature added. 279def _launch_ide_by_module_contents(args, ide_util_obj, jlist=None, clist=None, 280 both=False): 281 """Deals with the suitable IDE launch action. 282 283 The rules AIDEGen won't ask users to choose one of the languages are: 284 1. Users set CLion as IDE: CLion only supports C/C++. 285 2. Test mode is true: if AIDEGEN_TEST_MODE is true the default language is 286 Java. 287 288 Args: 289 args: A list of system arguments. 290 ide_util_obj: An ide_util instance. 291 jlist: A list of java build targets. 292 clist: A list of native build targets. 293 both: A boolean, True to launch both languages else False. 294 """ 295 if both: 296 _launch_vscode(ide_util_obj, project_info.ProjectInfo.modules_info, 297 jlist, clist) 298 return 299 if not jlist and not clist: 300 logging.warning('\nThere is neither java nor native module needs to be' 301 ' opened') 302 return 303 answer = None 304 if constant.IDE_NAME_DICT[args.ide[0]] == constant.IDE_CLION: 305 answer = constant.C_CPP 306 elif common_util.to_boolean( 307 os.environ.get(constant.AIDEGEN_TEST_MODE, 'false')): 308 answer = constant.JAVA 309 if not answer and jlist and clist: 310 answer = _get_preferred_ide_from_user(_LANGUAGE_OPTIONS) 311 if (jlist and not clist) or (answer == constant.JAVA): 312 _create_and_launch_java_projects(ide_util_obj, jlist) 313 return 314 if (clist and not jlist) or (answer == constant.C_CPP): 315 native_project_info.NativeProjectInfo.generate_projects(clist) 316 native_project_file = native_util.generate_clion_projects(clist) 317 if native_project_file: 318 _launch_native_projects(ide_util_obj, args, [native_project_file]) 319 320 321def _launch_vscode(ide_util_obj, atest_module_info, jtargets, ctargets): 322 """Launches targets with VSCode IDE. 323 324 Args: 325 ide_util_obj: An ide_util instance. 326 atest_module_info: A ModuleInfo instance contains the data of 327 module-info.json. 328 jtargets: A list of Java project targets. 329 ctargets: A list of native project targets. 330 """ 331 abs_paths = [] 332 for target in jtargets: 333 _, abs_path = common_util.get_related_paths(atest_module_info, target) 334 abs_paths.append(abs_path) 335 if ctargets: 336 cc_module_info = native_module_info.NativeModuleInfo() 337 native_project_info.NativeProjectInfo.generate_projects(ctargets) 338 vs_gen = vscode_native_project_file_gen.VSCodeNativeProjectFileGenerator 339 for target in ctargets: 340 _, abs_path = common_util.get_related_paths(cc_module_info, target) 341 vs_native = vs_gen(abs_path) 342 vs_native.generate_c_cpp_properties_json_file() 343 if abs_path not in abs_paths: 344 abs_paths.append(abs_path) 345 vs_path = vscode_workspace_file_gen.generate_code_workspace_file(abs_paths) 346 if not ide_util_obj: 347 return 348 _launch_ide(ide_util_obj, vs_path) 349 350 351@common_util.time_logged(message=_TIME_EXCEED_MSG, maximum=_MAX_TIME) 352def main_with_message(args): 353 """Main entry with skip build message. 354 355 Args: 356 args: A list of system arguments. 357 """ 358 aidegen_main(args) 359 360 361@common_util.time_logged 362def main_without_message(args): 363 """Main entry without skip build message. 364 365 Args: 366 args: A list of system arguments. 367 """ 368 aidegen_main(args) 369 370 371# pylint: disable=broad-except 372def main(argv): 373 """Main entry. 374 375 Show skip build message in aidegen main process if users command skip_build 376 otherwise remind them to use it and include metrics supports. 377 378 Args: 379 argv: A list of system arguments. 380 """ 381 exit_code = constant.EXIT_CODE_NORMAL 382 launch_ide = True 383 try: 384 args = _parse_args(argv) 385 launch_ide = not args.no_launch 386 common_util.configure_logging(args.verbose) 387 is_whole_android_tree = project_config.is_whole_android_tree( 388 args.targets, args.android_tree) 389 references = [constant.ANDROID_TREE] if is_whole_android_tree else [] 390 aidegen_metrics.starts_asuite_metrics(references) 391 if args.skip_build: 392 main_without_message(args) 393 else: 394 main_with_message(args) 395 except BaseException as err: 396 exit_code = constant.EXIT_CODE_EXCEPTION 397 _, exc_value, exc_traceback = sys.exc_info() 398 if isinstance(err, errors.AIDEgenError): 399 exit_code = constant.EXIT_CODE_AIDEGEN_EXCEPTION 400 # Filter out sys.Exit(0) case, which is not an exception case. 401 if isinstance(err, SystemExit) and exc_value.code == 0: 402 exit_code = constant.EXIT_CODE_NORMAL 403 if exit_code is not constant.EXIT_CODE_NORMAL: 404 error_message = str(exc_value) 405 traceback_list = traceback.format_tb(exc_traceback) 406 traceback_list.append(error_message) 407 traceback_str = ''.join(traceback_list) 408 aidegen_metrics.ends_asuite_metrics(exit_code, traceback_str, 409 error_message) 410 # print out the trackback message for developers to debug 411 print(traceback_str) 412 raise err 413 finally: 414 print('\n{0} {1}\n'.format(_INFO, AIDEGEN_REPORT_LINK)) 415 # Send the end message here on ignoring launch IDE case. 416 if not launch_ide and exit_code is constant.EXIT_CODE_NORMAL: 417 aidegen_metrics.ends_asuite_metrics(exit_code) 418 419 420def aidegen_main(args): 421 """AIDEGen main entry. 422 423 Try to generate project files for using in IDE. The process is: 424 1. Instantiate a ProjectConfig singleton object and initialize its 425 environment. After creating a singleton instance for ProjectConfig, 426 other modules can use project configurations by 427 ProjectConfig.get_instance(). 428 2. Get an IDE instance from ide_util, ide_util.get_ide_util_instance will 429 use ProjectConfig.get_instance() inside the function. 430 3. Setup project_info.ProjectInfo.modules_info by instantiate 431 AidegenModuleInfo. 432 4. Check if projects contain native projects and launch related IDE. 433 434 Args: 435 args: A list of system arguments. 436 """ 437 config = project_config.ProjectConfig(args) 438 config.init_environment() 439 targets = config.targets 440 # Called ide_util for pre-check the IDE existence state. 441 ide_util_obj = ide_util.get_ide_util_instance(args.ide[0]) 442 project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo() 443 cc_module_info = native_module_info.NativeModuleInfo() 444 jtargets, ctargets = native_util.get_native_and_java_projects( 445 project_info.ProjectInfo.modules_info, cc_module_info, targets) 446 both = config.ide_name == constant.IDE_VSCODE 447 # Backward compatible strategy, when both java and native module exist, 448 # check the preferred target from the user and launch single one. 449 _launch_ide_by_module_contents(args, ide_util_obj, jtargets, ctargets, both) 450 451 452if __name__ == '__main__': 453 main(sys.argv[1:]) 454