• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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