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"""It is an AIDEGen sub task : IDE operation task!
18
19Takes a project file path as input, after passing the needed check(file
20existence, IDE type, etc.), launch the project in related IDE.
21
22    Typical usage example:
23
24    ide_util_obj = IdeUtil()
25    if ide_util_obj.is_ide_installed():
26        ide_util_obj.config_ide(project_file)
27        ide_util_obj.launch_ide()
28
29        # Get the configuration folders of IntelliJ or Android Studio.
30        ide_util_obj.get_ide_config_folders()
31"""
32
33import glob
34import logging
35import os
36import platform
37import re
38import subprocess
39
40from xml.etree import ElementTree
41
42from aidegen import constant
43from aidegen import templates
44from aidegen.lib import aidegen_metrics
45from aidegen.lib import android_dev_os
46from aidegen.lib import common_util
47from aidegen.lib import config
48from aidegen.lib import errors
49from aidegen.lib import ide_common_util
50from aidegen.lib import project_config
51from aidegen.lib import project_file_gen
52from aidegen.sdk import jdk_table
53from aidegen.lib import xml_util
54
55# Add 'nohup' to prevent IDE from being terminated when console is terminated.
56_IDEA_FOLDER = '.idea'
57_IML_EXTENSION = '.iml'
58_JDK_PATH_TOKEN = '@JDKpath'
59_COMPONENT_END_TAG = '  </component>'
60_ECLIPSE_WS = '~/Documents/AIDEGen_Eclipse_workspace'
61_ALERT_CREATE_WS = ('AIDEGen will create a workspace at %s for Eclipse, '
62                    'Enter `y` to allow AIDEgen to automatically create the '
63                    'workspace for you. Otherwise, you need to select the '
64                    'workspace after Eclipse is launched.\nWould you like '
65                    'AIDEgen to automatically create the workspace for you?'
66                    '(y/n)' % constant.ECLIPSE_WS)
67_NO_LAUNCH_IDE_CMD = """
68Can not find IDE: {}, in path: {}, you can:
69    - add IDE executable to your $PATH
70or  - specify the exact IDE executable path by "aidegen -p"
71or  - specify "aidegen -n" to generate project file only
72"""
73_INFO_IMPORT_CONFIG = ('{} needs to import the application configuration for '
74                       'the new version!\nAfter the import is finished, rerun '
75                       'the command if your project did not launch. Please '
76                       'follow the showing dialog to finish the import action.'
77                       '\n\n')
78CONFIG_DIR = 'config'
79LINUX_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
80                              'prebuilts/jdk/jdk17/linux-x86')
81LINUX_JDK_TABLE_PATH = 'config/options/jdk.table.xml'
82LINUX_FILE_TYPE_PATH = 'config/options/filetypes.xml'
83LINUX_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Android/Sdk')
84MAC_JDK_PATH = os.path.join(common_util.get_android_root_dir(),
85                            'prebuilts/jdk/jdk17/darwin-x86')
86ALTERNATIVE_JDK_TABLE_PATH = 'options/jdk.table.xml'
87ALTERNATIVE_FILE_TYPE_XML_PATH = 'options/filetypes.xml'
88MAC_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Library/Android/sdk')
89PATTERN_KEY = 'pattern'
90TYPE_KEY = 'type'
91_TEST_MAPPING_FILE_TYPE = 'JSON'
92TEST_MAPPING_NAME = 'TEST_MAPPING'
93_TEST_MAPPING_TYPE = '<mapping pattern="TEST_MAPPING" type="JSON" />'
94_XPATH_EXTENSION_MAP = 'component/extensionMap'
95_XPATH_MAPPING = _XPATH_EXTENSION_MAP + '/mapping'
96_SPECIFIC_INTELLIJ_VERSION = 2020.1
97_TEST_MAPPING_FILE_TYPE_ADDING_WARN = '\n{} {}\n'.format(
98    common_util.COLORED_INFO('WARNING:'),
99    ('TEST_MAPPING file type can\'t be added to filetypes.xml. The reason '
100     'might be: lack of the parent tag to add TEST_MAPPING file type.'))
101
102
103# pylint: disable=too-many-lines
104# pylint: disable=invalid-name
105class IdeUtil:
106    """Provide a set of IDE operations, e.g., launch and configuration.
107
108    Attributes:
109        _ide: IdeBase derived instance, the related IDE object.
110
111    For example:
112        1. Check if IDE is installed.
113        2. Config IDE, e.g. config code style, SDK path, and etc.
114        3. Launch an IDE.
115    """
116
117    def __init__(self,
118                 installed_path=None,
119                 ide='j',
120                 config_reset=False,
121                 is_mac=False):
122        logging.debug('IdeUtil with OS name: %s%s', platform.system(),
123                      '(Mac)' if is_mac else '')
124        self._ide = _get_ide(installed_path, ide, config_reset, is_mac)
125
126    def is_ide_installed(self):
127        """Checks if the IDE is already installed.
128
129        Returns:
130            True if IDE is installed already, otherwise False.
131        """
132        return self._ide.is_ide_installed()
133
134    def launch_ide(self):
135        """Launches the relative IDE by opening the passed project file."""
136        return self._ide.launch_ide()
137
138    def config_ide(self, project_abspath):
139        """To config the IDE, e.g., setup code style, init SDK, and etc.
140
141        Args:
142            project_abspath: An absolute path of the project.
143        """
144        self._ide.project_abspath = project_abspath
145        if self.is_ide_installed() and self._ide:
146            self._ide.apply_optional_config()
147
148    def get_default_path(self):
149        """Gets IDE default installed path."""
150        return self._ide.default_installed_path
151
152    def ide_name(self):
153        """Gets IDE name."""
154        return self._ide.ide_name
155
156    def get_ide_config_folders(self):
157        """Gets the config folders of IDE."""
158        return self._ide.config_folders
159
160
161class IdeBase:
162    """The most base class of IDE, provides interface and partial path init.
163
164    Class Attributes:
165        _JDK_PATH: The path of JDK in android project.
166        _IDE_JDK_TABLE_PATH: The path of JDK table which record JDK info in IDE.
167        _IDE_FILE_TYPE_PATH: The path of filetypes.xml.
168        _JDK_CONTENT: A string, the content of the JDK configuration.
169        _DEFAULT_ANDROID_SDK_PATH: A string, the path of Android SDK.
170        _CONFIG_DIR: A string of the config folder name.
171        _SYMBOLIC_VERSIONS: A string list of the symbolic link paths of the
172                            relevant IDE.
173
174    Attributes:
175        _installed_path: String for the IDE binary path.
176        _config_reset: Boolean, True for reset configuration, else not reset.
177        _bin_file_name: String for IDE executable file name.
178        _bin_paths: A list of all possible IDE executable file absolute paths.
179        _ide_name: String for IDE name.
180        _bin_folders: A list of all possible IDE installed paths.
181        config_folders: A list of all possible paths for the IntelliJ
182                        configuration folder.
183        project_abspath: The absolute path of the project.
184
185    For example:
186        1. Check if IDE is installed.
187        2. Launch IDE.
188        3. Config IDE.
189    """
190
191    _JDK_PATH = ''
192    _IDE_JDK_TABLE_PATH = ''
193    _IDE_FILE_TYPE_PATH = ''
194    _JDK_CONTENT = ''
195    _DEFAULT_ANDROID_SDK_PATH = ''
196    _CONFIG_DIR = ''
197    _SYMBOLIC_VERSIONS = []
198
199    def __init__(self, installed_path=None, config_reset=False):
200        self._installed_path = installed_path
201        self._config_reset = config_reset
202        self._ide_name = ''
203        self._bin_file_name = ''
204        self._bin_paths = []
205        self._bin_folders = []
206        self.config_folders = []
207        self.project_abspath = ''
208
209    def is_ide_installed(self):
210        """Checks if IDE is already installed.
211
212        Returns:
213            True if IDE is installed already, otherwise False.
214        """
215        return bool(self._installed_path)
216
217    def launch_ide(self):
218        """Launches IDE by opening the passed project file."""
219        ide_common_util.launch_ide(self.project_abspath, self._get_ide_cmd(),
220                                   self._ide_name)
221
222    def apply_optional_config(self):
223        """Do IDEA global config action.
224
225        Run code style config, SDK config.
226        """
227        if not self._installed_path:
228            return
229        # Skip config action if there's no config folder exists.
230        _path_list = self._get_config_root_paths()
231        if not _path_list:
232            return
233        self.config_folders = _path_list.copy()
234
235        for _config_path in _path_list:
236            jdk_file = os.path.join(_config_path, self._IDE_JDK_TABLE_PATH)
237            jdk_xml = jdk_table.JDKTableXML(jdk_file, self._JDK_CONTENT,
238                                            self._JDK_PATH,
239                                            self._DEFAULT_ANDROID_SDK_PATH)
240            if jdk_xml.config_jdk_table_xml():
241                project_file_gen.gen_enable_debugger_module(
242                    self.project_abspath, jdk_xml.android_sdk_version)
243
244            # Set the max file size in the idea.properties.
245            intellij_config_dir = os.path.join(_config_path, self._CONFIG_DIR)
246            config.IdeaProperties(intellij_config_dir).set_max_file_size()
247
248            self._add_test_mapping_file_type(_config_path)
249
250    def _add_test_mapping_file_type(self, _config_path):
251        """Adds TEST_MAPPING file type.
252
253        IntelliJ can't recognize TEST_MAPPING files as the json file. It needs
254        adding file type mapping in filetypes.xml to recognize TEST_MAPPING
255        files.
256
257        Args:
258            _config_path: the path of IDE config.
259        """
260        file_type_path = os.path.join(_config_path, self._IDE_FILE_TYPE_PATH)
261        if not os.path.isfile(file_type_path):
262            logging.warning('The file: filetypes.xml is not found.')
263            return
264
265        file_type_xml = xml_util.parse_xml(file_type_path)
266        if not file_type_xml:
267            logging.warning('Can\'t parse filetypes.xml.')
268            return
269
270        root = file_type_xml.getroot()
271        add_pattern = True
272        for mapping in root.findall(_XPATH_MAPPING):
273            attrib = mapping.attrib
274            if PATTERN_KEY in attrib and TYPE_KEY in attrib:
275                if attrib[PATTERN_KEY] == TEST_MAPPING_NAME:
276                    if attrib[TYPE_KEY] != _TEST_MAPPING_FILE_TYPE:
277                        attrib[TYPE_KEY] = _TEST_MAPPING_FILE_TYPE
278                        file_type_xml.write(file_type_path)
279                    add_pattern = False
280                    break
281        if add_pattern:
282            ext_attrib = root.find(_XPATH_EXTENSION_MAP)
283            if ext_attrib is None:
284                print(_TEST_MAPPING_FILE_TYPE_ADDING_WARN)
285                return
286            ext_attrib.append(ElementTree.fromstring(_TEST_MAPPING_TYPE))
287            pretty_xml = common_util.to_pretty_xml(root)
288            common_util.file_generate(file_type_path, pretty_xml)
289
290    def _get_config_root_paths(self):
291        """Get the config root paths from derived class.
292
293        Returns:
294            A string list of IDE config paths, return multiple paths if more
295            than one path are found, return an empty list when none is found.
296        """
297        raise NotImplementedError()
298
299    @property
300    def default_installed_path(self):
301        """Gets IDE default installed path."""
302        return ' '.join(self._bin_folders)
303
304    @property
305    def ide_name(self):
306        """Gets IDE name."""
307        return self._ide_name
308
309    def _get_ide_cmd(self):
310        """Compose launch IDE command to run a new process and redirect output.
311
312        Returns:
313            A string of launch IDE command.
314        """
315        return ide_common_util.get_run_ide_cmd(self._installed_path,
316                                               self.project_abspath)
317
318    def _init_installed_path(self, installed_path):
319        """Initialize IDE installed path.
320
321        Args:
322            installed_path: the installed path to be checked.
323        """
324        if installed_path:
325            path_list = ide_common_util.get_script_from_input_path(
326                installed_path, self._bin_file_name)
327            self._installed_path = path_list[0] if path_list else None
328        else:
329            self._installed_path = self._get_script_from_system()
330        if not self._installed_path:
331            logging.error('No %s installed.', self._ide_name)
332            return
333
334        self._set_installed_path()
335
336    def _get_script_from_system(self):
337        """Get one IDE installed path from internal path.
338
339        Returns:
340            The sh full path, or None if not found.
341        """
342        sh_list = self._get_existent_scripts_in_system()
343        return sh_list[0] if sh_list else None
344
345    def _get_possible_bin_paths(self):
346        """Gets all possible IDE installed paths."""
347        return [os.path.join(f, self._bin_file_name) for f in self._bin_folders]
348
349    def _get_ide_from_environment_paths(self):
350        """Get IDE executable binary file from environment paths.
351
352        Returns:
353            A string of IDE executable binary path if found, otherwise return
354            None.
355        """
356        env_paths = os.environ['PATH'].split(':')
357        for env_path in env_paths:
358            path = ide_common_util.get_scripts_from_dir_path(
359                env_path, self._bin_file_name)
360            if path:
361                return path
362        return None
363
364    def _setup_ide(self):
365        """The callback used to run the necessary setup work of the IDE.
366
367        When ide_util.config_ide is called to set up the JDK, SDK and some
368        features, the main thread will callback the Idexxx._setup_ide
369        to provide the chance for running the necessary setup of the specific
370        IDE. Default is to do nothing.
371        """
372
373    def _get_existent_scripts_in_system(self):
374        """Gets the relevant IDE run script path from system.
375
376        First get correct IDE installed path from internal paths, if not found
377        search it from environment paths.
378
379        Returns:
380            The list of script full path, or None if not found.
381        """
382        return (ide_common_util.get_script_from_internal_path(self._bin_paths,
383                                                              self._ide_name) or
384                self._get_ide_from_environment_paths())
385
386    def _get_user_preference(self, versions):
387        """Make sure the version is valid and update preference if needed.
388
389        Args:
390            versions: A list of the IDE script path, contains the symbolic path.
391
392        Returns: An IDE script path, or None is not found.
393        """
394        if not versions:
395            return None
396        if len(versions) == 1:
397            return versions[0]
398        with config.AidegenConfig() as conf:
399            if not self._config_reset and (conf.preferred_version(self.ide_name)
400                                           in versions):
401                return conf.preferred_version(self.ide_name)
402            display_versions = self._merge_symbolic_version(versions)
403            preferred = ide_common_util.ask_preference(display_versions,
404                                                       self.ide_name)
405            if preferred:
406                conf.set_preferred_version(self._get_real_path(preferred),
407                                           self.ide_name)
408
409            return conf.preferred_version(self.ide_name)
410
411    def _set_installed_path(self):
412        """Write the user's input installed path into the config file.
413
414        If users input an existent IDE installed path, we should keep it in
415        the configuration.
416        """
417        if self._installed_path:
418            with config.AidegenConfig() as aconf:
419                aconf.set_preferred_version(self._installed_path, self.ide_name)
420
421    def _merge_symbolic_version(self, versions):
422        """Merge the duplicate version of symbolic links.
423
424        Stable and beta versions are a symbolic link to an existing version.
425        This function assemble symbolic and real to make it more clear to read.
426        Ex:
427        ['/opt/intellij-ce-stable/bin/idea.sh',
428        '/opt/intellij-ce-2019.1/bin/idea.sh'] to
429        ['/opt/intellij-ce-stable/bin/idea.sh ->
430        /opt/intellij-ce-2019.1/bin/idea.sh',
431        '/opt/intellij-ce-2019.1/bin/idea.sh']
432
433        Args:
434            versions: A list of all installed versions.
435
436        Returns:
437            A list of versions to show for user to select. It may contain
438            'symbolic_path/idea.sh -> original_path/idea.sh'.
439        """
440        display_versions = versions[:]
441        for symbolic in self._SYMBOLIC_VERSIONS:
442            if symbolic in display_versions and (os.path.isfile(symbolic)):
443                real_path = os.path.realpath(symbolic)
444                for index, path in enumerate(display_versions):
445                    if path == symbolic:
446                        display_versions[index] = ' -> '.join(
447                            [display_versions[index], real_path])
448                        break
449        return display_versions
450
451    @staticmethod
452    def _get_real_path(path):
453        """ Get real path from merged path.
454
455        Turn the path string "/opt/intellij-ce-stable/bin/idea.sh -> /opt/
456        intellij-ce-2019.2/bin/idea.sh" into
457        "/opt/intellij-ce-stable/bin/idea.sh"
458
459        Args:
460            path: A path string may be merged with symbolic path.
461        Returns:
462            The real IntelliJ installed path.
463        """
464        return path.split()[0]
465
466
467class IdeIntelliJ(IdeBase):
468    """Provide basic IntelliJ ops, e.g., launch IDEA, and config IntelliJ.
469
470    For example:
471        1. Check if IntelliJ is installed.
472        2. Launch an IntelliJ.
473        3. Config IntelliJ.
474    """
475    def __init__(self, installed_path=None, config_reset=False):
476        super().__init__(installed_path, config_reset)
477        self._ide_name = constant.IDE_INTELLIJ
478        self._ls_ce_path = ''
479        self._ls_ue_path = ''
480
481    def _get_config_root_paths(self):
482        """Get the config root paths from derived class.
483
484        Returns:
485            A string list of IDE config paths, return multiple paths if more
486            than one path are found, return an empty list when none is found.
487        """
488        raise NotImplementedError()
489
490    def _get_preferred_version(self):
491        """Get the user's preferred IntelliJ version.
492
493        Locates the IntelliJ IDEA launch script path by following rule.
494
495        1. If config file recorded user's preference version, load it.
496        2. If config file didn't record, search them form default path if there
497           are more than one version, ask user and record it.
498
499        Returns:
500            The sh full path, or None if no IntelliJ version is installed.
501        """
502        ce_paths = ide_common_util.get_intellij_version_path(self._ls_ce_path)
503        ue_paths = ide_common_util.get_intellij_version_path(self._ls_ue_path)
504        all_versions = self._get_all_versions(ce_paths, ue_paths)
505        for version in list(all_versions):
506            real_version = os.path.realpath(version)
507            if (os.path.islink(version.split('/bin')[0]) and
508                (real_version in all_versions)):
509                all_versions.remove(real_version)
510            if config.AidegenConfig.deprecated_intellij_version(real_version):
511                all_versions.remove(version)
512        return self._get_user_preference(all_versions)
513
514    def _setup_ide(self):
515        """The callback used to run the necessary setup work for the IDE.
516
517        IntelliJ has a default flow to let the user import the configuration
518        from the previous version, aidegen makes sure not to break the behavior
519        by checking in this callback implementation.
520        """
521        run_script_path = os.path.realpath(self._installed_path)
522        app_folder = self._get_application_path(run_script_path)
523        if not app_folder:
524            logging.warning('\nInvalid IDE installed path.')
525            return
526
527        show_hint = False
528        ide_version = self._get_ide_version(app_folder)
529        folder_path = self._get_config_dir(ide_version, app_folder)
530        import_process = None
531        while not os.path.isdir(folder_path):
532            # Guide the user to go through the IDE flow.
533            if not show_hint:
534                print('\n{} {}'.format(common_util.COLORED_INFO('INFO:'),
535                                       _INFO_IMPORT_CONFIG.format(
536                                           self.ide_name)))
537                try:
538                    import_process = subprocess.Popen(
539                        ide_common_util.get_run_ide_cmd(run_script_path, '',
540                                                        False), shell=True)
541                except (subprocess.SubprocessError, ValueError):
542                    logging.warning('\nSubprocess call gets the invalid input.')
543                finally:
544                    show_hint = True
545        if import_process:
546            try:
547                import_process.wait(1)
548            except subprocess.TimeoutExpired:
549                import_process.terminate()
550        return
551
552    def _get_script_from_system(self):
553        """Get correct IntelliJ installed path from internal path.
554
555        Returns:
556            The sh full path, or None if no IntelliJ version is installed.
557        """
558        found = self._get_preferred_version()
559        if found:
560            logging.debug('IDE internal installed path: %s.', found)
561        return found
562
563    @staticmethod
564    def _get_all_versions(cefiles, uefiles):
565        """Get all versions of launch script files.
566
567        Args:
568            cefiles: CE version launch script paths.
569            uefiles: UE version launch script paths.
570
571        Returns:
572            A list contains all versions of launch script files.
573        """
574        all_versions = []
575        if cefiles:
576            all_versions.extend(cefiles)
577        if uefiles:
578            all_versions.extend(uefiles)
579        return all_versions
580
581    @staticmethod
582    def _get_application_path(run_script_path):
583        """Get the relevant configuration folder based on the run script path.
584
585        Args:
586            run_script_path: The string of the run script path for the IntelliJ.
587
588        Returns:
589            The string of the IntelliJ application folder name or None if the
590            run_script_path is invalid. The returned folder format is as
591            follows,
592                1. .IdeaIC2019.3
593                2. .IntelliJIdea2019.3
594                3. IntelliJIdea2020.1
595        """
596        if not run_script_path or not os.path.isfile(run_script_path):
597            return None
598        index = str.find(run_script_path, 'intellij-')
599        target_path = None if index == -1 else run_script_path[index:]
600        if not target_path or '-' not in run_script_path:
601            return None
602        return IdeIntelliJ._get_config_folder_name(target_path)
603
604    @staticmethod
605    def _get_ide_version(config_folder_name):
606        """Gets IntelliJ version from the input app folder name.
607
608        Args:
609            config_folder_name: A string of the app folder name.
610
611        Returns:
612            A string of the IntelliJ version.
613        """
614        versions = re.findall(r'\d+', config_folder_name)
615        if not versions:
616            logging.warning('\nInvalid IntelliJ config folder name: %s.',
617                            config_folder_name)
618            return None
619        return '.'.join(versions)
620
621    @staticmethod
622    def _get_config_folder_name(script_folder_name):
623        """Gets IntelliJ config folder name from the IDE version.
624
625        The config folder name has been changed since 2020.1.
626
627        Args:
628            script_folder_name: A string of the script folder name of IntelliJ.
629
630        Returns:
631            A string of the IntelliJ config folder name.
632        """
633        path_data = script_folder_name.split('-')
634        if not path_data or len(path_data) < 3:
635            return None
636        ide_version = path_data[2].split(os.sep)[0]
637        numbers = ide_version.split('.')
638        if len(numbers) > 2:
639            ide_version = '.'.join([numbers[0], numbers[1]])
640        try:
641            version = float(ide_version)
642        except ValueError:
643            return None
644        pre_folder = '.IdeaIC'
645        if version < _SPECIFIC_INTELLIJ_VERSION:
646            if path_data[1] == 'ue':
647                pre_folder = '.IntelliJIdea'
648        else:
649            if path_data[1] == 'ce':
650                pre_folder = 'IdeaIC'
651            elif path_data[1] == 'ue':
652                pre_folder = 'IntelliJIdea'
653        return ''.join([pre_folder, ide_version])
654
655    @staticmethod
656    def _get_config_dir(ide_version, config_folder_name):
657        """Gets IntelliJ config directory by the config folder name.
658
659        The IntelliJ config directory is changed from version 2020.1. Get the
660        version from app folder name and determine the config directory.
661        URL: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519
662
663        Args:
664            ide_version: A string of the IntelliJ's version.
665            config_folder_name: A string of the IntelliJ's config folder name.
666
667        Returns:
668            A string of the IntelliJ config directory.
669        """
670        try:
671            version = float(ide_version)
672        except ValueError:
673            return None
674        if version < _SPECIFIC_INTELLIJ_VERSION:
675            return os.path.join(
676                os.getenv('HOME'), config_folder_name)
677        return os.path.join(
678            os.getenv('HOME'), '.config', 'JetBrains', config_folder_name)
679
680
681class IdeLinuxIntelliJ(IdeIntelliJ):
682    """Provide the IDEA behavior implementation for OS Linux.
683
684    Class Attributes:
685        _INTELLIJ_RE: Regular expression of IntelliJ installed name in GLinux.
686
687    For example:
688        1. Check if IntelliJ is installed.
689        2. Launch an IntelliJ.
690        3. Config IntelliJ.
691    """
692
693    _JDK_PATH = LINUX_JDK_PATH
694    # TODO(b/127899277): Preserve a config for jdk version option case.
695    _CONFIG_DIR = CONFIG_DIR
696    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
697    _IDE_FILE_TYPE_PATH = LINUX_FILE_TYPE_PATH
698    _JDK_CONTENT = templates.LINUX_JDK_XML
699    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
700    _SYMBOLIC_VERSIONS = ['/opt/intellij-ce-stable/bin/idea.sh',
701                          '/opt/intellij-ue-stable/bin/idea.sh',
702                          '/opt/intellij-ce-beta/bin/idea.sh',
703                          '/opt/intellij-ue-beta/bin/idea.sh']
704    _INTELLIJ_RE = re.compile(r'intellij-(ce|ue)-')
705
706    def __init__(self, installed_path=None, config_reset=False):
707        super().__init__(installed_path, config_reset)
708        self._bin_file_name = 'idea.sh'
709        self._bin_folders = ['/opt/intellij-*/bin']
710        self._ls_ce_path = os.path.join('/opt/intellij-ce-*/bin',
711                                        self._bin_file_name)
712        self._ls_ue_path = os.path.join('/opt/intellij-ue-*/bin',
713                                        self._bin_file_name)
714        self._init_installed_path(installed_path)
715
716    def _get_config_root_paths(self):
717        """To collect the global config folder paths of IDEA as a string list.
718
719        The config folder of IntelliJ IDEA is under the user's home directory,
720        .IdeaIC20xx.x and .IntelliJIdea20xx.x are folder names for different
721        versions.
722
723        Returns:
724            A string list for IDE config root paths, and return an empty list
725            when none is found.
726        """
727        if not self._installed_path:
728            return None
729
730        _config_folders = []
731        _config_folder = ''
732        if IdeLinuxIntelliJ._INTELLIJ_RE.search(self._installed_path):
733            _path_data = os.path.realpath(self._installed_path)
734            _config_folder = self._get_application_path(_path_data)
735            if not _config_folder:
736                return None
737            ide_version = self._get_ide_version(_config_folder)
738            if not ide_version:
739                return None
740            try:
741                version = float(ide_version)
742            except ValueError:
743                return None
744            folder_path = self._get_config_dir(ide_version, _config_folder)
745            if version >= _SPECIFIC_INTELLIJ_VERSION:
746                self._IDE_JDK_TABLE_PATH = ALTERNATIVE_JDK_TABLE_PATH
747                self._IDE_FILE_TYPE_PATH = ALTERNATIVE_FILE_TYPE_XML_PATH
748
749            if not os.path.isdir(folder_path):
750                logging.debug("\nThe config folder: %s doesn't exist",
751                              _config_folder)
752                self._setup_ide()
753
754            _config_folders.append(folder_path)
755        else:
756            # TODO(b/123459239): For the case that the user provides the IDEA
757            # binary path, we now collect all possible IDEA config root paths.
758            _config_folders = glob.glob(
759                os.path.join(os.getenv('HOME'), '.IdeaI?20*'))
760            _config_folders.extend(
761                glob.glob(os.path.join(os.getenv('HOME'), '.IntelliJIdea20*')))
762            _config_folders.extend(
763                glob.glob(os.path.join(os.getenv('HOME'), '.config',
764                                       'IntelliJIdea202*')))
765            logging.debug('The config path list: %s.', _config_folders)
766
767        return _config_folders
768
769
770class IdeMacIntelliJ(IdeIntelliJ):
771    """Provide the IDEA behavior implementation for OS Mac.
772
773    For example:
774        1. Check if IntelliJ is installed.
775        2. Launch an IntelliJ.
776        3. Config IntelliJ.
777    """
778
779    _JDK_PATH = MAC_JDK_PATH
780    _IDE_JDK_TABLE_PATH = ALTERNATIVE_JDK_TABLE_PATH
781    _IDE_FILE_TYPE_PATH = ALTERNATIVE_FILE_TYPE_XML_PATH
782    _JDK_CONTENT = templates.MAC_JDK_XML
783    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
784
785    def __init__(self, installed_path=None, config_reset=False):
786        super().__init__(installed_path, config_reset)
787        self._bin_file_name = 'idea'
788        self._bin_folders = ['/Applications/IntelliJ IDEA.app/Contents/MacOS']
789        self._bin_paths = self._get_possible_bin_paths()
790        self._ls_ce_path = os.path.join(
791            '/Applications/IntelliJ IDEA CE.app/Contents/MacOS',
792            self._bin_file_name)
793        self._ls_ue_path = os.path.join(
794            '/Applications/IntelliJ IDEA.app/Contents/MacOS',
795            self._bin_file_name)
796        self._init_installed_path(installed_path)
797
798    def _get_config_root_paths(self):
799        """To collect the global config folder paths of IDEA as a string list.
800
801        Returns:
802            A string list for IDE config root paths, and return an empty list
803            when none is found.
804        """
805        if not self._installed_path:
806            return None
807
808        _config_folders = []
809        if 'IntelliJ' in self._installed_path:
810            _config_folders = glob.glob(
811                os.path.join(
812                    os.getenv('HOME'), 'Library/Preferences/IdeaI?20*'))
813            _config_folders.extend(
814                glob.glob(
815                    os.path.join(
816                        os.getenv('HOME'),
817                        'Library/Preferences/IntelliJIdea20*')))
818        return _config_folders
819
820
821class IdeStudio(IdeBase):
822    """Class offers a set of Android Studio launching utilities.
823
824    For example:
825        1. Check if Android Studio is installed.
826        2. Launch an Android Studio.
827        3. Config Android Studio.
828    """
829
830    def __init__(self, installed_path=None, config_reset=False):
831        super().__init__(installed_path, config_reset)
832        self._ide_name = constant.IDE_ANDROID_STUDIO
833
834    def _get_config_root_paths(self):
835        """Get the config root paths from derived class.
836
837        Returns:
838            A string list of IDE config paths, return multiple paths if more
839            than one path are found, return an empty list when none is found.
840        """
841        raise NotImplementedError()
842
843    def _get_script_from_system(self):
844        """Get correct Studio installed path from internal path.
845
846        Returns:
847            The sh full path, or None if no Studio version is installed.
848        """
849        found = self._get_preferred_version()
850        if found:
851            logging.debug('IDE internal installed path: %s.', found)
852        return found
853
854    def _get_preferred_version(self):
855        """Get the user's preferred Studio version.
856
857        Locates the Studio launch script path by following rule.
858
859        1. If config file recorded user's preference version, load it.
860        2. If config file didn't record, search them form default path if there
861           are more than one version, ask user and record it.
862
863        Returns:
864            The sh full path, or None if no Studio version is installed.
865        """
866        all_versions = self._get_existent_scripts_in_system()
867        if not all_versions:
868            return None
869        for version in list(all_versions):
870            real_version = os.path.realpath(version)
871            if (os.path.islink(version.split('/bin')[0]) and
872                (real_version in all_versions)):
873                all_versions.remove(real_version)
874            if config.AidegenConfig.deprecated_studio_version(real_version):
875                all_versions.remove(version)
876        return self._get_user_preference(all_versions)
877
878    def apply_optional_config(self):
879        """Do the configuration of Android Studio.
880
881        Configures code style and SDK for Java project and do nothing for
882        others.
883        """
884        if not self.project_abspath:
885            return
886        # TODO(b/150662865): The following workaround should be replaced.
887        # Since the path of the artifact for Java is the .idea directory but
888        # native is a CMakeLists.txt file using this to workaround first.
889        if os.path.isfile(self.project_abspath):
890            return
891        if os.path.isdir(self.project_abspath):
892            IdeBase.apply_optional_config(self)
893
894
895class IdeLinuxStudio(IdeStudio):
896    """Class offers a set of Android Studio launching utilities for OS Linux.
897
898    For example:
899        1. Check if Android Studio is installed.
900        2. Launch an Android Studio.
901        3. Config Android Studio.
902    """
903
904    _JDK_PATH = LINUX_JDK_PATH
905    _CONFIG_DIR = CONFIG_DIR
906    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
907    _JDK_CONTENT = templates.LINUX_JDK_XML
908    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
909    _SYMBOLIC_VERSIONS = [
910        '/opt/android-studio-with-blaze-stable/bin/studio.sh',
911        '/opt/android-studio-stable/bin/studio.sh',
912        '/opt/android-studio-with-blaze-beta/bin/studio.sh',
913        '/opt/android-studio-beta/bin/studio.sh']
914
915    def __init__(self, installed_path=None, config_reset=False):
916        super().__init__(installed_path, config_reset)
917        self._bin_file_name = 'studio.sh'
918        self._bin_folders = ['/opt/android-studio*/bin']
919        self._bin_paths = self._get_possible_bin_paths()
920        self._init_installed_path(installed_path)
921
922    def _get_config_root_paths(self):
923        """Collect the global config folder paths as a string list.
924
925        Returns:
926            A string list for IDE config root paths, and return an empty list
927            when none is found.
928        """
929        return glob.glob(os.path.join(os.getenv('HOME'), '.AndroidStudio*'))
930
931
932class IdeMacStudio(IdeStudio):
933    """Class offers a set of Android Studio launching utilities for OS Mac.
934
935    For example:
936        1. Check if Android Studio is installed.
937        2. Launch an Android Studio.
938        3. Config Android Studio.
939    """
940
941    _JDK_PATH = MAC_JDK_PATH
942    _IDE_JDK_TABLE_PATH = ALTERNATIVE_JDK_TABLE_PATH
943    _JDK_CONTENT = templates.MAC_JDK_XML
944    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
945
946    def __init__(self, installed_path=None, config_reset=False):
947        super().__init__(installed_path, config_reset)
948        self._bin_file_name = 'studio'
949        self._bin_folders = ['/Applications/Android Studio.app/Contents/MacOS']
950        self._bin_paths = self._get_possible_bin_paths()
951        self._init_installed_path(installed_path)
952
953    def _get_config_root_paths(self):
954        """Collect the global config folder paths as a string list.
955
956        Returns:
957            A string list for IDE config root paths, and return an empty list
958            when none is found.
959        """
960        return glob.glob(
961            os.path.join(
962                os.getenv('HOME'), 'Library/Preferences/AndroidStudio*'))
963
964
965class IdeEclipse(IdeBase):
966    """Class offers a set of Eclipse launching utilities.
967
968    Attributes:
969        cmd: A list of the build command.
970
971    For example:
972        1. Check if Eclipse is installed.
973        2. Launch an Eclipse.
974    """
975
976    def __init__(self, installed_path=None, config_reset=False):
977        super().__init__(installed_path, config_reset)
978        self._ide_name = constant.IDE_ECLIPSE
979        self._bin_file_name = 'eclipse'
980        self.cmd = []
981
982    def _get_script_from_system(self):
983        """Get correct IDE installed path from internal path.
984
985        Remove any file with extension, the filename should be like, 'eclipse',
986        'eclipse47' and so on, check if the file is executable and filter out
987        file such as 'eclipse.ini'.
988
989        Returns:
990            The sh full path, or None if no IntelliJ version is installed.
991        """
992        for ide_path in self._bin_paths:
993            # The binary name of Eclipse could be eclipse47, eclipse49,
994            # eclipse47_testing or eclipse49_testing. So finding the matched
995            # binary by /path/to/ide/eclipse*.
996            ls_output = glob.glob(ide_path + '*', recursive=True)
997            if ls_output:
998                ls_output = sorted(ls_output)
999                match_eclipses = []
1000                for path in ls_output:
1001                    if os.access(path, os.X_OK):
1002                        match_eclipses.append(path)
1003                if match_eclipses:
1004                    match_eclipses = sorted(match_eclipses)
1005                    logging.debug('Result for checking %s after sort: %s.',
1006                                  self._ide_name, match_eclipses[0])
1007                    return match_eclipses[0]
1008        return None
1009
1010    def _get_ide_cmd(self):
1011        """Compose launch IDE command to run a new process and redirect output.
1012
1013        AIDEGen will create a default workspace
1014        ~/Documents/AIDEGen_Eclipse_workspace for users if they agree to do
1015        that. Also, we could not import the default project through the command
1016        line so remove the project path argument.
1017
1018        Returns:
1019            A string of launch IDE command.
1020        """
1021        if (os.path.exists(os.path.expanduser(constant.ECLIPSE_WS))
1022                or str(input(_ALERT_CREATE_WS)).lower() == 'y'):
1023            self.cmd.extend(['-data', constant.ECLIPSE_WS])
1024        self.cmd.extend([constant.IGNORE_STD_OUT_ERR_CMD, '&'])
1025        return ' '.join(self.cmd)
1026
1027    def apply_optional_config(self):
1028        """Override to do nothing."""
1029
1030    def _get_config_root_paths(self):
1031        """Override to do nothing."""
1032
1033
1034class IdeLinuxEclipse(IdeEclipse):
1035    """Class offers a set of Eclipse launching utilities for OS Linux.
1036
1037    For example:
1038        1. Check if Eclipse is installed.
1039        2. Launch an Eclipse.
1040    """
1041
1042    def __init__(self, installed_path=None, config_reset=False):
1043        super().__init__(installed_path, config_reset)
1044        self._bin_folders = ['/opt/eclipse*', '/usr/bin/']
1045        self._bin_paths = self._get_possible_bin_paths()
1046        self._init_installed_path(installed_path)
1047        self.cmd = [constant.NOHUP, self._installed_path.replace(' ', r'\ ')]
1048
1049
1050class IdeMacEclipse(IdeEclipse):
1051    """Class offers a set of Eclipse launching utilities for OS Mac.
1052
1053    For example:
1054        1. Check if Eclipse is installed.
1055        2. Launch an Eclipse.
1056    """
1057
1058    def __init__(self, installed_path=None, config_reset=False):
1059        super().__init__(installed_path, config_reset)
1060        self._bin_file_name = 'eclipse'
1061        self._bin_folders = [os.path.expanduser('~/eclipse/**')]
1062        self._bin_paths = self._get_possible_bin_paths()
1063        self._init_installed_path(installed_path)
1064        self.cmd = [self._installed_path.replace(' ', r'\ ')]
1065
1066
1067class IdeCLion(IdeBase):
1068    """Class offers a set of CLion launching utilities.
1069
1070    For example:
1071        1. Check if CLion is installed.
1072        2. Launch an CLion.
1073    """
1074
1075    def __init__(self, installed_path=None, config_reset=False):
1076        super().__init__(installed_path, config_reset)
1077        self._ide_name = constant.IDE_CLION
1078
1079    def apply_optional_config(self):
1080        """Override to do nothing."""
1081
1082    def _get_config_root_paths(self):
1083        """Override to do nothing."""
1084
1085
1086class IdeLinuxCLion(IdeCLion):
1087    """Class offers a set of CLion launching utilities for OS Linux.
1088
1089    For example:
1090        1. Check if CLion is installed.
1091        2. Launch an CLion.
1092    """
1093
1094    def __init__(self, installed_path=None, config_reset=False):
1095        super().__init__(installed_path, config_reset)
1096        self._bin_file_name = 'clion.sh'
1097        # TODO(b/141288011): Handle /opt/clion-*/bin to let users choose a
1098        # preferred version of CLion in the future.
1099        self._bin_folders = ['/opt/clion-stable/bin']
1100        self._bin_paths = self._get_possible_bin_paths()
1101        self._init_installed_path(installed_path)
1102
1103
1104class IdeMacCLion(IdeCLion):
1105    """Class offers a set of Android Studio launching utilities for OS Mac.
1106
1107    For example:
1108        1. Check if Android Studio is installed.
1109        2. Launch an Android Studio.
1110    """
1111
1112    def __init__(self, installed_path=None, config_reset=False):
1113        super().__init__(installed_path, config_reset)
1114        self._bin_file_name = 'clion'
1115        self._bin_folders = ['/Applications/CLion.app/Contents/MacOS/CLion']
1116        self._bin_paths = self._get_possible_bin_paths()
1117        self._init_installed_path(installed_path)
1118
1119
1120class IdeVSCode(IdeBase):
1121    """Class offers a set of VSCode launching utilities.
1122
1123    For example:
1124        1. Check if VSCode is installed.
1125        2. Launch an VSCode.
1126    """
1127
1128    def __init__(self, installed_path=None, config_reset=False):
1129        super().__init__(installed_path, config_reset)
1130        self._ide_name = constant.IDE_VSCODE
1131
1132    def apply_optional_config(self):
1133        """Override to do nothing."""
1134
1135    def _get_config_root_paths(self):
1136        """Override to do nothing."""
1137
1138
1139class IdeLinuxVSCode(IdeVSCode):
1140    """Class offers a set of VSCode launching utilities for OS Linux."""
1141
1142    def __init__(self, installed_path=None, config_reset=False):
1143        super().__init__(installed_path, config_reset)
1144        self._bin_file_name = 'code'
1145        self._bin_folders = ['/usr/bin']
1146        self._bin_paths = self._get_possible_bin_paths()
1147        self._init_installed_path(installed_path)
1148
1149
1150class IdeMacVSCode(IdeVSCode):
1151    """Class offers a set of VSCode launching utilities for OS Mac."""
1152
1153    def __init__(self, installed_path=None, config_reset=False):
1154        super().__init__(installed_path, config_reset)
1155        self._bin_file_name = 'code'
1156        self._bin_folders = ['/usr/local/bin']
1157        self._bin_paths = self._get_possible_bin_paths()
1158        self._init_installed_path(installed_path)
1159
1160
1161def get_ide_util_instance(ide='j'):
1162    """Get an IdeUtil class instance for launching IDE.
1163
1164    Args:
1165        ide: A key character of IDE to be launched. Default ide='j' is to
1166            launch IntelliJ.
1167
1168    Returns:
1169        An IdeUtil class instance.
1170    """
1171    conf = project_config.ProjectConfig.get_instance()
1172    if not conf.is_launch_ide:
1173        return None
1174    is_mac = (android_dev_os.AndroidDevOS.MAC == android_dev_os.AndroidDevOS.
1175              get_os_type())
1176    tool = IdeUtil(conf.ide_installed_path, ide, conf.config_reset, is_mac)
1177    if not tool.is_ide_installed():
1178        ipath = conf.ide_installed_path or tool.get_default_path()
1179        err = _NO_LAUNCH_IDE_CMD.format(constant.IDE_NAME_DICT[ide], ipath)
1180        logging.error(err)
1181        stack_trace = common_util.remove_user_home_path(err)
1182        logs = '%s exists? %s' % (common_util.remove_user_home_path(ipath),
1183                                  os.path.exists(ipath))
1184        aidegen_metrics.ends_asuite_metrics(constant.IDE_LAUNCH_FAILURE,
1185                                            stack_trace,
1186                                            logs)
1187        raise errors.IDENotExistError(err)
1188    return tool
1189
1190
1191def _get_ide(installed_path=None, ide='j', config_reset=False, is_mac=False):
1192    """Get IDE to be launched according to the ide input and OS type.
1193
1194    Args:
1195        installed_path: The IDE installed path to be checked.
1196        ide: A key character of IDE to be launched. Default ide='j' is to
1197            launch IntelliJ.
1198        config_reset: A boolean, if true reset configuration data.
1199
1200    Returns:
1201        A corresponding IDE instance.
1202    """
1203    if is_mac:
1204        return _get_mac_ide(installed_path, ide, config_reset)
1205    return _get_linux_ide(installed_path, ide, config_reset)
1206
1207
1208def _get_mac_ide(installed_path=None, ide='j', config_reset=False):
1209    """Get IDE to be launched according to the ide input for OS Mac.
1210
1211    Args:
1212        installed_path: The IDE installed path to be checked.
1213        ide: A key character of IDE to be launched. Default ide='j' is to
1214            launch IntelliJ.
1215        config_reset: A boolean, if true reset configuration data.
1216
1217    Returns:
1218        A corresponding IDE instance.
1219    """
1220    if ide == 'e':
1221        return IdeMacEclipse(installed_path, config_reset)
1222    if ide == 's':
1223        return IdeMacStudio(installed_path, config_reset)
1224    if ide == 'c':
1225        return IdeMacCLion(installed_path, config_reset)
1226    if ide == 'v':
1227        return IdeMacVSCode(installed_path, config_reset)
1228    return IdeMacIntelliJ(installed_path, config_reset)
1229
1230
1231def _get_linux_ide(installed_path=None, ide='j', config_reset=False):
1232    """Get IDE to be launched according to the ide input for OS Linux.
1233
1234    Args:
1235        installed_path: The IDE installed path to be checked.
1236        ide: A key character of IDE to be launched. Default ide='j' is to
1237            launch IntelliJ.
1238        config_reset: A boolean, if true reset configuration data.
1239
1240    Returns:
1241        A corresponding IDE instance.
1242    """
1243    if ide == 'e':
1244        return IdeLinuxEclipse(installed_path, config_reset)
1245    if ide == 's':
1246        return IdeLinuxStudio(installed_path, config_reset)
1247    if ide == 'c':
1248        return IdeLinuxCLion(installed_path, config_reset)
1249    if ide == 'v':
1250        return IdeLinuxVSCode(installed_path, config_reset)
1251    return IdeLinuxIntelliJ(installed_path, config_reset)
1252