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=['u'],
135        help=('Launch IDE type, j: IntelliJ, s: Android Studio, e: Eclipse, '
136              'c: CLion, v: VS Code. The default value is \'u\': undefined.'))
137    parser.add_argument(
138        '-p',
139        '--ide-path',
140        dest='ide_installed_path',
141        help='IDE installed path.')
142    parser.add_argument(
143        '-n', '--no_launch', action='store_true', help='Do not launch IDE.')
144    parser.add_argument(
145        '-r',
146        '--config-reset',
147        dest='config_reset',
148        action='store_true',
149        help='Reset all saved configurations, e.g., preferred IDE version.')
150    parser.add_argument(
151        '-s',
152        '--skip-build',
153        dest='skip_build',
154        action='store_true',
155        help=('Skip building jars or modules that create java files in build '
156              'time, e.g. R/AIDL/Logtags.'))
157    parser.add_argument(
158        '-a',
159        '--android-tree',
160        dest='android_tree',
161        action='store_true',
162        help='Generate whole Android source tree project file for IDE.')
163    parser.add_argument(
164        '-e',
165        '--exclude-paths',
166        dest='exclude_paths',
167        nargs='*',
168        help='Exclude the directories in IDE.')
169    parser.add_argument(
170        '-V',
171        '--version',
172        action='store_true',
173        help='Print aidegen version string.')
174    parser.add_argument(
175        '-l',
176        '--language',
177        default=['u'],
178        help=('Launch IDE with a specific language, j: Java, c: C/C++. The '
179              'default value is \'u\': undefined.'))
180    return parser.parse_args(args)
181
182
183def _generate_project_files(projects):
184    """Generate project files by IDE type.
185
186    Args:
187        projects: A list of ProjectInfo instances.
188    """
189    config = project_config.ProjectConfig.get_instance()
190    if config.ide_name == constant.IDE_ECLIPSE:
191        eclipse_project_file_gen.EclipseConf.generate_ide_project_files(
192            projects)
193    else:
194        project_file_gen.ProjectFileGenerator.generate_ide_project_files(
195            projects)
196
197
198def _launch_ide(ide_util_obj, project_absolute_path):
199    """Launch IDE through ide_util instance.
200
201    To launch IDE,
202    1. Set IDE config.
203    2. For IntelliJ, use .idea as open target is better than .iml file,
204       because open the latter is like to open a kind of normal file.
205    3. Show _LAUNCH_SUCCESS_MSG to remind users IDE being launched.
206
207    Args:
208        ide_util_obj: An ide_util instance.
209        project_absolute_path: A string of project absolute path.
210    """
211    ide_util_obj.config_ide(project_absolute_path)
212    if ide_util_obj.ide_name() == constant.IDE_ECLIPSE:
213        launch_msg = ' '.join([_LAUNCH_SUCCESS_MSG,
214                               _LAUNCH_ECLIPSE_SUCCESS_MSG.format(
215                                   PROJECT_PATH=project_absolute_path)])
216    else:
217        launch_msg = _LAUNCH_SUCCESS_MSG
218    print('\n{} {}\n'.format(_CONGRATULATIONS, launch_msg))
219    print('\n{} {}\n'.format(_INFO, _IDE_CACHE_REMINDER_MSG))
220    # Send the end message to Clearcut server before launching IDE to make sure
221    # the execution time is correct.
222    aidegen_metrics.ends_asuite_metrics(constant.EXIT_CODE_EXCEPTION)
223    ide_util_obj.launch_ide()
224
225
226def _launch_native_projects(ide_util_obj, args, cmakelists):
227    """Launches C/C++ projects with IDE.
228
229    AIDEGen provides the IDE argument for CLion, but there's still a implicit
230    way to launch it. The rules to launch it are:
231    1. If no target IDE, we don't have to launch any IDE for C/C++ project.
232    2. If the target IDE is IntelliJ or Eclipse, we should launch C/C++
233       projects with CLion.
234
235    Args:
236        ide_util_obj: An ide_util instance.
237        args: An argparse.Namespace class instance holding parsed args.
238        cmakelists: A list of CMakeLists.txt file paths.
239    """
240    if not ide_util_obj:
241        return
242    native_ide_util_obj = ide_util_obj
243    ide_name = constant.IDE_NAME_DICT[args.ide[0]]
244    if ide_name in _LAUNCH_CLION_IDES:
245        native_ide_util_obj = ide_util.get_ide_util_instance('c')
246    if native_ide_util_obj:
247        _launch_ide(native_ide_util_obj, ' '.join(cmakelists))
248
249
250def _create_and_launch_java_projects(ide_util_obj, targets):
251    """Launches Android of Java(Kotlin) projects with IDE.
252
253    Args:
254        ide_util_obj: An ide_util instance.
255        targets: A list of build targets.
256    """
257    projects = project_info.ProjectInfo.generate_projects(targets)
258    project_info.ProjectInfo.multi_projects_locate_source(projects)
259    _generate_project_files(projects)
260    if ide_util_obj:
261        _launch_ide(ide_util_obj, projects[0].project_absolute_path)
262
263
264def _get_preferred_ide_from_user(all_choices):
265    """Provides the option list to get back users single choice.
266
267    Args:
268        all_choices: A list of string type for all options.
269
270    Return:
271        A string of the user's single choice item.
272    """
273    if not all_choices:
274        return None
275    options = []
276    items = []
277    for index, option in enumerate(all_choices, 1):
278        options.append('{}. {}'.format(index, option))
279        items.append(str(index))
280    query = _CHOOSE_LANGUAGE_MSG.format(len(options), '\n'.join(options))
281    input_data = input(query)
282    while input_data not in items:
283        input_data = input('Please select one.\t')
284    return all_choices[int(input_data) - 1]
285
286
287def _launch_ide_by_module_contents(args, ide_util_obj, jlist=None, clist=None,
288                                   both=False):
289    """Deals with the suitable IDE launch action.
290
291    The rules of AIDEGen launching IDE with languages are:
292      1. aidegen frameworks/base
293         aidegen frameworks/base -l j
294         launch Java projects of frameworks/base in IntelliJ.
295      2. aidegen frameworks/base -i c
296         aidegen frameworks/base -l c
297         launch C/C++ projects of frameworks/base in CLion.
298      3. aidegen frameworks/base -i s
299         launch Java projects of frameworks/base in Android Studio.
300         aidegen frameworks/base -i s -l c
301         launch C/C++ projects of frameworks/base in Android Studio.
302      4. aidegen frameworks/base -i j -l c
303         launch Java projects of frameworks/base in IntelliJ.
304      5. aidegen frameworks/base -i c -l j
305         launch C/C++ projects of frameworks/base in CLion.
306
307    Args:
308        args: A list of system arguments.
309        ide_util_obj: An ide_util instance.
310        jlist: A list of java build targets.
311        clist: A list of C/C++ build targets.
312        both: A boolean, True to launch both languages else False.
313    """
314    if both:
315        _launch_vscode(ide_util_obj, project_info.ProjectInfo.modules_info,
316                       jlist, clist)
317        return
318    if not jlist and not clist:
319        logging.warning('\nThere is neither java nor C/C++ module needs to be'
320                        ' opened')
321        return
322
323    language, _ = common_util.determine_language_ide(
324        args.language[0], args.ide[0])
325    if (jlist and not clist) or (language == constant.JAVA):
326        _create_and_launch_java_projects(ide_util_obj, jlist)
327        return
328    if (clist and not jlist) or (language == constant.C_CPP):
329        native_project_info.NativeProjectInfo.generate_projects(clist)
330        native_project_file = native_util.generate_clion_projects(clist)
331        if native_project_file:
332            _launch_native_projects(ide_util_obj, args, [native_project_file])
333
334
335def _launch_vscode(ide_util_obj, atest_module_info, jtargets, ctargets):
336    """Launches targets with VSCode IDE.
337
338    Args:
339        ide_util_obj: An ide_util instance.
340        atest_module_info: A ModuleInfo instance contains the data of
341                module-info.json.
342        jtargets: A list of Java project targets.
343        ctargets: A list of C/C++ project targets.
344    """
345    abs_paths = []
346    for target in jtargets:
347        _, abs_path = common_util.get_related_paths(atest_module_info, target)
348        abs_paths.append(abs_path)
349    if ctargets:
350        cc_module_info = native_module_info.NativeModuleInfo()
351        native_project_info.NativeProjectInfo.generate_projects(ctargets)
352        vs_gen = vscode_native_project_file_gen.VSCodeNativeProjectFileGenerator
353        for target in ctargets:
354            _, abs_path = common_util.get_related_paths(cc_module_info, target)
355            vs_native = vs_gen(abs_path)
356            vs_native.generate_c_cpp_properties_json_file()
357            if abs_path not in abs_paths:
358                abs_paths.append(abs_path)
359    vs_path = vscode_workspace_file_gen.generate_code_workspace_file(abs_paths)
360    if not ide_util_obj:
361        return
362    _launch_ide(ide_util_obj, vs_path)
363
364
365@common_util.time_logged(message=_TIME_EXCEED_MSG, maximum=_MAX_TIME)
366def main_with_message(args):
367    """Main entry with skip build message.
368
369    Args:
370        args: A list of system arguments.
371    """
372    aidegen_main(args)
373
374
375@common_util.time_logged
376def main_without_message(args):
377    """Main entry without skip build message.
378
379    Args:
380        args: A list of system arguments.
381    """
382    aidegen_main(args)
383
384
385# pylint: disable=broad-except
386def main(argv):
387    """Main entry.
388
389    Show skip build message in aidegen main process if users command skip_build
390    otherwise remind them to use it and include metrics supports.
391
392    Args:
393        argv: A list of system arguments.
394    """
395    exit_code = constant.EXIT_CODE_NORMAL
396    launch_ide = True
397    ask_version = False
398    try:
399        args = _parse_args(argv)
400        if args.version:
401            ask_version = True
402            version_file = os.path.join(os.path.dirname(__file__),
403                                        constant.VERSION_FILE)
404            print(common_util.read_file_content(version_file))
405            sys.exit(constant.EXIT_CODE_NORMAL)
406
407        launch_ide = not args.no_launch
408        common_util.configure_logging(args.verbose)
409        is_whole_android_tree = project_config.is_whole_android_tree(
410            args.targets, args.android_tree)
411        references = [constant.ANDROID_TREE] if is_whole_android_tree else []
412        aidegen_metrics.starts_asuite_metrics(references)
413        if args.skip_build:
414            main_without_message(args)
415        else:
416            main_with_message(args)
417    except BaseException as err:
418        exit_code = constant.EXIT_CODE_EXCEPTION
419        _, exc_value, exc_traceback = sys.exc_info()
420        if isinstance(err, errors.AIDEgenError):
421            exit_code = constant.EXIT_CODE_AIDEGEN_EXCEPTION
422        # Filter out sys.Exit(0) case, which is not an exception case.
423        if isinstance(err, SystemExit) and exc_value.code == 0:
424            exit_code = constant.EXIT_CODE_NORMAL
425        if exit_code is not constant.EXIT_CODE_NORMAL:
426            error_message = str(exc_value)
427            traceback_list = traceback.format_tb(exc_traceback)
428            traceback_list.append(error_message)
429            traceback_str = ''.join(traceback_list)
430            aidegen_metrics.ends_asuite_metrics(exit_code, traceback_str,
431                                                error_message)
432            # print out the trackback message for developers to debug
433            print(traceback_str)
434            raise err
435    finally:
436        if not ask_version:
437            print('\n{0} {1}\n'.format(_INFO, AIDEGEN_REPORT_LINK))
438            # Send the end message here on ignoring launch IDE case.
439            if not launch_ide and exit_code is constant.EXIT_CODE_NORMAL:
440                aidegen_metrics.ends_asuite_metrics(exit_code)
441
442
443def aidegen_main(args):
444    """AIDEGen main entry.
445
446    Try to generate project files for using in IDE. The process is:
447      1. Instantiate a ProjectConfig singleton object and initialize its
448         environment. After creating a singleton instance for ProjectConfig,
449         other modules can use project configurations by
450         ProjectConfig.get_instance().
451      2. Get an IDE instance from ide_util, ide_util.get_ide_util_instance will
452         use ProjectConfig.get_instance() inside the function.
453      3. Setup project_info.ProjectInfo.modules_info by instantiate
454         AidegenModuleInfo.
455      4. Check if projects contain C/C++ projects and launch related IDE.
456
457    Args:
458        args: A list of system arguments.
459    """
460    config = project_config.ProjectConfig(args)
461    config.init_environment()
462    targets = config.targets
463    # Called ide_util for pre-check the IDE existence state.
464    _, ide = common_util.determine_language_ide(args.language[0], args.ide[0])
465    ide_util_obj = ide_util.get_ide_util_instance(constant.IDE_DICT[ide])
466    project_info.ProjectInfo.modules_info = module_info.AidegenModuleInfo()
467    cc_module_info = native_module_info.NativeModuleInfo()
468    jtargets, ctargets = native_util.get_native_and_java_projects(
469        project_info.ProjectInfo.modules_info, cc_module_info, targets)
470    both = config.ide_name == constant.IDE_VSCODE
471    # Backward compatible strategy, when both java and C/C++ module exist,
472    # check the preferred target from the user and launch single one.
473    _launch_ide_by_module_contents(args, ide_util_obj, jtargets, ctargets, both)
474
475
476if __name__ == '__main__':
477    main(sys.argv[1:])
478