• 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"""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/jdk8/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/jdk8/darwin-x86')
86ALTERNAIVE_JDK_TABLE_PATH = 'options/jdk.table.xml'
87ALTERNAIVE_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 not ext_attrib:
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 no 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        tmp_versions = all_versions.copy()
506        for version in tmp_versions:
507            real_version = os.path.realpath(version)
508            if config.AidegenConfig.deprecated_intellij_version(real_version):
509                all_versions.remove(version)
510        return self._get_user_preference(all_versions)
511
512    def _setup_ide(self):
513        """The callback used to run the necessary setup work for the IDE.
514
515        IntelliJ has a default flow to let the user import the configuration
516        from the previous version, aidegen makes sure not to break the behavior
517        by checking in this callback implementation.
518        """
519        run_script_path = os.path.realpath(self._installed_path)
520        app_folder = self._get_application_path(run_script_path)
521        if not app_folder:
522            logging.warning('\nInvalid IDE installed path.')
523            return
524
525        show_hint = False
526        ide_version = self._get_ide_version(app_folder)
527        folder_path = self._get_config_dir(ide_version, app_folder)
528        import_process = None
529        while not os.path.isdir(folder_path):
530            # Guide the user to go through the IDE flow.
531            if not show_hint:
532                print('\n{} {}'.format(common_util.COLORED_INFO('INFO:'),
533                                       _INFO_IMPORT_CONFIG.format(
534                                           self.ide_name)))
535                try:
536                    import_process = subprocess.Popen(
537                        ide_common_util.get_run_ide_cmd(run_script_path, '',
538                                                        False), shell=True)
539                except (subprocess.SubprocessError, ValueError):
540                    logging.warning('\nSubprocess call gets the invalid input.')
541                finally:
542                    show_hint = True
543        if import_process:
544            try:
545                import_process.wait(1)
546            except subprocess.TimeoutExpired:
547                import_process.terminate()
548        return
549
550    def _get_script_from_system(self):
551        """Get correct IntelliJ installed path from internal path.
552
553        Returns:
554            The sh full path, or None if no IntelliJ version is installed.
555        """
556        found = self._get_preferred_version()
557        if found:
558            logging.debug('IDE internal installed path: %s.', found)
559        return found
560
561    @staticmethod
562    def _get_all_versions(cefiles, uefiles):
563        """Get all versions of launch script files.
564
565        Args:
566            cefiles: CE version launch script paths.
567            uefiles: UE version launch script paths.
568
569        Returns:
570            A list contains all versions of launch script files.
571        """
572        all_versions = []
573        if cefiles:
574            all_versions.extend(cefiles)
575        if uefiles:
576            all_versions.extend(uefiles)
577        return all_versions
578
579    @staticmethod
580    def _get_application_path(run_script_path):
581        """Get the relevant configuration folder based on the run script path.
582
583        Args:
584            run_script_path: The string of the run script path for the IntelliJ.
585
586        Returns:
587            The string of the IntelliJ application folder name or None if the
588            run_script_path is invalid. The returned folder format is as
589            follows,
590                1. .IdeaIC2019.3
591                2. .IntelliJIdea2019.3
592                3. IntelliJIdea2020.1
593        """
594        if not run_script_path or not os.path.isfile(run_script_path):
595            return None
596        index = str.find(run_script_path, 'intellij-')
597        target_path = None if index == -1 else run_script_path[index:]
598        if not target_path or '-' not in run_script_path:
599            return None
600        return IdeIntelliJ._get_config_folder_name(target_path)
601
602    @staticmethod
603    def _get_ide_version(config_folder_name):
604        """Gets IntelliJ version from the input app folder name.
605
606        Args:
607            config_folder_name: A string of the app folder name.
608
609        Returns:
610            A string of the IntelliJ version.
611        """
612        versions = re.findall(r'\d+', config_folder_name)
613        if not versions:
614            logging.warning('\nInvalid IntelliJ config folder name: %s.',
615                            config_folder_name)
616            return None
617        return '.'.join(versions)
618
619    @staticmethod
620    def _get_config_folder_name(script_folder_name):
621        """Gets IntelliJ config folder name from the IDE version.
622
623        The config folder name has been changed since 2020.1.
624
625        Args:
626            script_folder_name: A string of the script folder name of IntelliJ.
627
628        Returns:
629            A string of the IntelliJ config folder name.
630        """
631        path_data = script_folder_name.split('-')
632        if not path_data or len(path_data) < 3:
633            return None
634        ide_version = path_data[2].split(os.sep)[0]
635        numbers = ide_version.split('.')
636        if len(numbers) > 2:
637            ide_version = '.'.join([numbers[0], numbers[1]])
638        try:
639            version = float(ide_version)
640        except ValueError:
641            return None
642        pre_folder = '.IdeaIC'
643        if version < _SPECIFIC_INTELLIJ_VERSION:
644            if path_data[1] == 'ue':
645                pre_folder = '.IntelliJIdea'
646        else:
647            if path_data[1] == 'ce':
648                pre_folder = 'IdeaIC'
649            elif path_data[1] == 'ue':
650                pre_folder = 'IntelliJIdea'
651        return ''.join([pre_folder, ide_version])
652
653    @staticmethod
654    def _get_config_dir(ide_version, config_folder_name):
655        """Gets IntelliJ config directory by the config folder name.
656
657        The IntelliJ config directory is changed from version 2020.1. Get the
658        version from app folder name and determine the config directory.
659        URL: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519
660
661        Args:
662            ide_version: A string of the IntelliJ's version.
663            config_folder_name: A string of the IntelliJ's config folder name.
664
665        Returns:
666            A string of the IntelliJ config directory.
667        """
668        try:
669            version = float(ide_version)
670        except ValueError:
671            return None
672        if version < _SPECIFIC_INTELLIJ_VERSION:
673            return os.path.join(
674                os.getenv('HOME'), config_folder_name)
675        return os.path.join(
676            os.getenv('HOME'), '.config', 'JetBrains', config_folder_name)
677
678
679class IdeLinuxIntelliJ(IdeIntelliJ):
680    """Provide the IDEA behavior implementation for OS Linux.
681
682    Class Attributes:
683        _INTELLIJ_RE: Regular expression of IntelliJ installed name in GLinux.
684
685    For example:
686        1. Check if IntelliJ is installed.
687        2. Launch an IntelliJ.
688        3. Config IntelliJ.
689    """
690
691    _JDK_PATH = LINUX_JDK_PATH
692    # TODO(b/127899277): Preserve a config for jdk version option case.
693    _CONFIG_DIR = CONFIG_DIR
694    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
695    _IDE_FILE_TYPE_PATH = LINUX_FILE_TYPE_PATH
696    _JDK_CONTENT = templates.LINUX_JDK_XML
697    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
698    _SYMBOLIC_VERSIONS = ['/opt/intellij-ce-stable/bin/idea.sh',
699                          '/opt/intellij-ue-stable/bin/idea.sh',
700                          '/opt/intellij-ce-beta/bin/idea.sh',
701                          '/opt/intellij-ue-beta/bin/idea.sh']
702    _INTELLIJ_RE = re.compile(r'intellij-(ce|ue)-')
703
704    def __init__(self, installed_path=None, config_reset=False):
705        super().__init__(installed_path, config_reset)
706        self._bin_file_name = 'idea.sh'
707        self._bin_folders = ['/opt/intellij-*/bin']
708        self._ls_ce_path = os.path.join('/opt/intellij-ce-*/bin',
709                                        self._bin_file_name)
710        self._ls_ue_path = os.path.join('/opt/intellij-ue-*/bin',
711                                        self._bin_file_name)
712        self._init_installed_path(installed_path)
713
714    def _get_config_root_paths(self):
715        """To collect the global config folder paths of IDEA as a string list.
716
717        The config folder of IntelliJ IDEA is under the user's home directory,
718        .IdeaIC20xx.x and .IntelliJIdea20xx.x are folder names for different
719        versions.
720
721        Returns:
722            A string list for IDE config root paths, and return an empty list
723            when none is found.
724        """
725        if not self._installed_path:
726            return None
727
728        _config_folders = []
729        _config_folder = ''
730        if IdeLinuxIntelliJ._INTELLIJ_RE.search(self._installed_path):
731            _path_data = os.path.realpath(self._installed_path)
732            _config_folder = self._get_application_path(_path_data)
733            if not _config_folder:
734                return None
735            ide_version = self._get_ide_version(_config_folder)
736            if not ide_version:
737                return None
738            try:
739                version = float(ide_version)
740            except ValueError:
741                return None
742            folder_path = self._get_config_dir(ide_version, _config_folder)
743            if version >= _SPECIFIC_INTELLIJ_VERSION:
744                self._IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
745                self._IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH
746
747            if not os.path.isdir(folder_path):
748                logging.debug("\nThe config folder: %s doesn't exist",
749                              _config_folder)
750                self._setup_ide()
751
752            _config_folders.append(folder_path)
753        else:
754            # TODO(b/123459239): For the case that the user provides the IDEA
755            # binary path, we now collect all possible IDEA config root paths.
756            _config_folders = glob.glob(
757                os.path.join(os.getenv('HOME'), '.IdeaI?20*'))
758            _config_folders.extend(
759                glob.glob(os.path.join(os.getenv('HOME'), '.IntelliJIdea20*')))
760            _config_folders.extend(
761                glob.glob(os.path.join(os.getenv('HOME'), '.config',
762                                       'IntelliJIdea202*')))
763            logging.debug('The config path list: %s.', _config_folders)
764
765        return _config_folders
766
767
768class IdeMacIntelliJ(IdeIntelliJ):
769    """Provide the IDEA behavior implementation for OS Mac.
770
771    For example:
772        1. Check if IntelliJ is installed.
773        2. Launch an IntelliJ.
774        3. Config IntelliJ.
775    """
776
777    _JDK_PATH = MAC_JDK_PATH
778    _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
779    _IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH
780    _JDK_CONTENT = templates.MAC_JDK_XML
781    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
782
783    def __init__(self, installed_path=None, config_reset=False):
784        super().__init__(installed_path, config_reset)
785        self._bin_file_name = 'idea'
786        self._bin_folders = ['/Applications/IntelliJ IDEA.app/Contents/MacOS']
787        self._bin_paths = self._get_possible_bin_paths()
788        self._ls_ce_path = os.path.join(
789            '/Applications/IntelliJ IDEA CE.app/Contents/MacOS',
790            self._bin_file_name)
791        self._ls_ue_path = os.path.join(
792            '/Applications/IntelliJ IDEA.app/Contents/MacOS',
793            self._bin_file_name)
794        self._init_installed_path(installed_path)
795
796    def _get_config_root_paths(self):
797        """To collect the global config folder paths of IDEA as a string list.
798
799        Returns:
800            A string list for IDE config root paths, and return an empty list
801            when none is found.
802        """
803        if not self._installed_path:
804            return None
805
806        _config_folders = []
807        if 'IntelliJ' in self._installed_path:
808            _config_folders = glob.glob(
809                os.path.join(
810                    os.getenv('HOME'), 'Library/Preferences/IdeaI?20*'))
811            _config_folders.extend(
812                glob.glob(
813                    os.path.join(
814                        os.getenv('HOME'),
815                        'Library/Preferences/IntelliJIdea20*')))
816        return _config_folders
817
818
819class IdeStudio(IdeBase):
820    """Class offers a set of Android Studio launching utilities.
821
822    For example:
823        1. Check if Android Studio is installed.
824        2. Launch an Android Studio.
825        3. Config Android Studio.
826    """
827
828    def __init__(self, installed_path=None, config_reset=False):
829        super().__init__(installed_path, config_reset)
830        self._ide_name = constant.IDE_ANDROID_STUDIO
831
832    def _get_config_root_paths(self):
833        """Get the config root paths from derived class.
834
835        Returns:
836            A string list of IDE config paths, return multiple paths if more
837            than one path are found, return an empty list when none is found.
838        """
839        raise NotImplementedError()
840
841    def _get_script_from_system(self):
842        """Get correct Studio installed path from internal path.
843
844        Returns:
845            The sh full path, or None if no Studio version is installed.
846        """
847        found = self._get_preferred_version()
848        if found:
849            logging.debug('IDE internal installed path: %s.', found)
850        return found
851
852    def _get_preferred_version(self):
853        """Get the user's preferred Studio version.
854
855        Locates the Studio launch script path by following rule.
856
857        1. If config file recorded user's preference version, load it.
858        2. If config file didn't record, search them form default path if there
859           are more than one version, ask user and record it.
860
861        Returns:
862            The sh full path, or None if no Studio version is installed.
863        """
864        versions = self._get_existent_scripts_in_system()
865        if not versions:
866            return None
867        for version in versions:
868            real_version = os.path.realpath(version)
869            if config.AidegenConfig.deprecated_studio_version(real_version):
870                versions.remove(version)
871        return self._get_user_preference(versions)
872
873    def apply_optional_config(self):
874        """Do the configuration of Android Studio.
875
876        Configures code style and SDK for Java project and do nothing for
877        others.
878        """
879        if not self.project_abspath:
880            return
881        # TODO(b/150662865): The following workaround should be replaced.
882        # Since the path of the artifact for Java is the .idea directory but
883        # native is a CMakeLists.txt file using this to workaround first.
884        if os.path.isfile(self.project_abspath):
885            return
886        if os.path.isdir(self.project_abspath):
887            IdeBase.apply_optional_config(self)
888
889
890class IdeLinuxStudio(IdeStudio):
891    """Class offers a set of Android Studio launching utilities for OS Linux.
892
893    For example:
894        1. Check if Android Studio is installed.
895        2. Launch an Android Studio.
896        3. Config Android Studio.
897    """
898
899    _JDK_PATH = LINUX_JDK_PATH
900    _CONFIG_DIR = CONFIG_DIR
901    _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH
902    _JDK_CONTENT = templates.LINUX_JDK_XML
903    _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH
904    _SYMBOLIC_VERSIONS = [
905        '/opt/android-studio-with-blaze-stable/bin/studio.sh',
906        '/opt/android-studio-stable/bin/studio.sh',
907        '/opt/android-studio-with-blaze-beta/bin/studio.sh',
908        '/opt/android-studio-beta/bin/studio.sh']
909
910    def __init__(self, installed_path=None, config_reset=False):
911        super().__init__(installed_path, config_reset)
912        self._bin_file_name = 'studio.sh'
913        self._bin_folders = ['/opt/android-studio*/bin']
914        self._bin_paths = self._get_possible_bin_paths()
915        self._init_installed_path(installed_path)
916
917    def _get_config_root_paths(self):
918        """Collect the global config folder paths as a string list.
919
920        Returns:
921            A string list for IDE config root paths, and return an empty list
922            when none is found.
923        """
924        return glob.glob(os.path.join(os.getenv('HOME'), '.AndroidStudio*'))
925
926
927class IdeMacStudio(IdeStudio):
928    """Class offers a set of Android Studio launching utilities for OS Mac.
929
930    For example:
931        1. Check if Android Studio is installed.
932        2. Launch an Android Studio.
933        3. Config Android Studio.
934    """
935
936    _JDK_PATH = MAC_JDK_PATH
937    _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH
938    _JDK_CONTENT = templates.MAC_JDK_XML
939    _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH
940
941    def __init__(self, installed_path=None, config_reset=False):
942        super().__init__(installed_path, config_reset)
943        self._bin_file_name = 'studio'
944        self._bin_folders = ['/Applications/Android Studio.app/Contents/MacOS']
945        self._bin_paths = self._get_possible_bin_paths()
946        self._init_installed_path(installed_path)
947
948    def _get_config_root_paths(self):
949        """Collect the global config folder paths as a string list.
950
951        Returns:
952            A string list for IDE config root paths, and return an empty list
953            when none is found.
954        """
955        return glob.glob(
956            os.path.join(
957                os.getenv('HOME'), 'Library/Preferences/AndroidStudio*'))
958
959
960class IdeEclipse(IdeBase):
961    """Class offers a set of Eclipse launching utilities.
962
963    Attributes:
964        cmd: A list of the build command.
965
966    For example:
967        1. Check if Eclipse is installed.
968        2. Launch an Eclipse.
969    """
970
971    def __init__(self, installed_path=None, config_reset=False):
972        super().__init__(installed_path, config_reset)
973        self._ide_name = constant.IDE_ECLIPSE
974        self._bin_file_name = 'eclipse'
975        self.cmd = []
976
977    def _get_script_from_system(self):
978        """Get correct IDE installed path from internal path.
979
980        Remove any file with extension, the filename should be like, 'eclipse',
981        'eclipse47' and so on, check if the file is executable and filter out
982        file such as 'eclipse.ini'.
983
984        Returns:
985            The sh full path, or None if no IntelliJ version is installed.
986        """
987        for ide_path in self._bin_paths:
988            # The binary name of Eclipse could be eclipse47, eclipse49,
989            # eclipse47_testing or eclipse49_testing. So finding the matched
990            # binary by /path/to/ide/eclipse*.
991            ls_output = glob.glob(ide_path + '*', recursive=True)
992            if ls_output:
993                ls_output = sorted(ls_output)
994                match_eclipses = []
995                for path in ls_output:
996                    if os.access(path, os.X_OK):
997                        match_eclipses.append(path)
998                if match_eclipses:
999                    match_eclipses = sorted(match_eclipses)
1000                    logging.debug('Result for checking %s after sort: %s.',
1001                                  self._ide_name, match_eclipses[0])
1002                    return match_eclipses[0]
1003        return None
1004
1005    def _get_ide_cmd(self):
1006        """Compose launch IDE command to run a new process and redirect output.
1007
1008        AIDEGen will create a default workspace
1009        ~/Documents/AIDEGen_Eclipse_workspace for users if they agree to do
1010        that. Also, we could not import the default project through the command
1011        line so remove the project path argument.
1012
1013        Returns:
1014            A string of launch IDE command.
1015        """
1016        if (os.path.exists(os.path.expanduser(constant.ECLIPSE_WS))
1017                or str(input(_ALERT_CREATE_WS)).lower() == 'y'):
1018            self.cmd.extend(['-data', constant.ECLIPSE_WS])
1019        self.cmd.extend([constant.IGNORE_STD_OUT_ERR_CMD, '&'])
1020        return ' '.join(self.cmd)
1021
1022    def apply_optional_config(self):
1023        """Override to do nothing."""
1024
1025    def _get_config_root_paths(self):
1026        """Override to do nothing."""
1027
1028
1029class IdeLinuxEclipse(IdeEclipse):
1030    """Class offers a set of Eclipse launching utilities for OS Linux.
1031
1032    For example:
1033        1. Check if Eclipse is installed.
1034        2. Launch an Eclipse.
1035    """
1036
1037    def __init__(self, installed_path=None, config_reset=False):
1038        super().__init__(installed_path, config_reset)
1039        self._bin_folders = ['/opt/eclipse*', '/usr/bin/']
1040        self._bin_paths = self._get_possible_bin_paths()
1041        self._init_installed_path(installed_path)
1042        self.cmd = [constant.NOHUP, self._installed_path.replace(' ', r'\ ')]
1043
1044
1045class IdeMacEclipse(IdeEclipse):
1046    """Class offers a set of Eclipse launching utilities for OS Mac.
1047
1048    For example:
1049        1. Check if Eclipse is installed.
1050        2. Launch an Eclipse.
1051    """
1052
1053    def __init__(self, installed_path=None, config_reset=False):
1054        super().__init__(installed_path, config_reset)
1055        self._bin_file_name = 'eclipse'
1056        self._bin_folders = [os.path.expanduser('~/eclipse/**')]
1057        self._bin_paths = self._get_possible_bin_paths()
1058        self._init_installed_path(installed_path)
1059        self.cmd = [self._installed_path.replace(' ', r'\ ')]
1060
1061
1062class IdeCLion(IdeBase):
1063    """Class offers a set of CLion launching utilities.
1064
1065    For example:
1066        1. Check if CLion is installed.
1067        2. Launch an CLion.
1068    """
1069
1070    def __init__(self, installed_path=None, config_reset=False):
1071        super().__init__(installed_path, config_reset)
1072        self._ide_name = constant.IDE_CLION
1073
1074    def apply_optional_config(self):
1075        """Override to do nothing."""
1076
1077    def _get_config_root_paths(self):
1078        """Override to do nothing."""
1079
1080
1081class IdeLinuxCLion(IdeCLion):
1082    """Class offers a set of CLion launching utilities for OS Linux.
1083
1084    For example:
1085        1. Check if CLion is installed.
1086        2. Launch an CLion.
1087    """
1088
1089    def __init__(self, installed_path=None, config_reset=False):
1090        super().__init__(installed_path, config_reset)
1091        self._bin_file_name = 'clion.sh'
1092        # TODO(b/141288011): Handle /opt/clion-*/bin to let users choose a
1093        # preferred version of CLion in the future.
1094        self._bin_folders = ['/opt/clion-stable/bin']
1095        self._bin_paths = self._get_possible_bin_paths()
1096        self._init_installed_path(installed_path)
1097
1098
1099class IdeMacCLion(IdeCLion):
1100    """Class offers a set of Android Studio launching utilities for OS Mac.
1101
1102    For example:
1103        1. Check if Android Studio is installed.
1104        2. Launch an Android Studio.
1105    """
1106
1107    def __init__(self, installed_path=None, config_reset=False):
1108        super().__init__(installed_path, config_reset)
1109        self._bin_file_name = 'clion'
1110        self._bin_folders = ['/Applications/CLion.app/Contents/MacOS/CLion']
1111        self._bin_paths = self._get_possible_bin_paths()
1112        self._init_installed_path(installed_path)
1113
1114
1115class IdeVSCode(IdeBase):
1116    """Class offers a set of VSCode launching utilities.
1117
1118    For example:
1119        1. Check if VSCode is installed.
1120        2. Launch an VSCode.
1121    """
1122
1123    def __init__(self, installed_path=None, config_reset=False):
1124        super().__init__(installed_path, config_reset)
1125        self._ide_name = constant.IDE_VSCODE
1126
1127    def apply_optional_config(self):
1128        """Override to do nothing."""
1129
1130    def _get_config_root_paths(self):
1131        """Override to do nothing."""
1132
1133
1134class IdeLinuxVSCode(IdeVSCode):
1135    """Class offers a set of VSCode launching utilities for OS Linux."""
1136
1137    def __init__(self, installed_path=None, config_reset=False):
1138        super().__init__(installed_path, config_reset)
1139        self._bin_file_name = 'code'
1140        self._bin_folders = ['/usr/bin']
1141        self._bin_paths = self._get_possible_bin_paths()
1142        self._init_installed_path(installed_path)
1143
1144
1145class IdeMacVSCode(IdeVSCode):
1146    """Class offers a set of VSCode launching utilities for OS Mac."""
1147
1148    def __init__(self, installed_path=None, config_reset=False):
1149        super().__init__(installed_path, config_reset)
1150        self._bin_file_name = 'code'
1151        self._bin_folders = ['/usr/local/bin']
1152        self._bin_paths = self._get_possible_bin_paths()
1153        self._init_installed_path(installed_path)
1154
1155
1156def get_ide_util_instance(ide='j'):
1157    """Get an IdeUtil class instance for launching IDE.
1158
1159    Args:
1160        ide: A key character of IDE to be launched. Default ide='j' is to
1161            launch IntelliJ.
1162
1163    Returns:
1164        An IdeUtil class instance.
1165    """
1166    conf = project_config.ProjectConfig.get_instance()
1167    if not conf.is_launch_ide:
1168        return None
1169    is_mac = (android_dev_os.AndroidDevOS.MAC == android_dev_os.AndroidDevOS.
1170              get_os_type())
1171    tool = IdeUtil(conf.ide_installed_path, ide, conf.config_reset, is_mac)
1172    if not tool.is_ide_installed():
1173        ipath = conf.ide_installed_path or tool.get_default_path()
1174        err = _NO_LAUNCH_IDE_CMD.format(constant.IDE_NAME_DICT[ide], ipath)
1175        logging.error(err)
1176        stack_trace = common_util.remove_user_home_path(err)
1177        logs = '%s exists? %s' % (common_util.remove_user_home_path(ipath),
1178                                  os.path.exists(ipath))
1179        aidegen_metrics.ends_asuite_metrics(constant.IDE_LAUNCH_FAILURE,
1180                                            stack_trace,
1181                                            logs)
1182        raise errors.IDENotExistError(err)
1183    return tool
1184
1185
1186def _get_ide(installed_path=None, ide='j', config_reset=False, is_mac=False):
1187    """Get IDE to be launched according to the ide input and OS type.
1188
1189    Args:
1190        installed_path: The IDE installed path to be checked.
1191        ide: A key character of IDE to be launched. Default ide='j' is to
1192            launch IntelliJ.
1193        config_reset: A boolean, if true reset configuration data.
1194
1195    Returns:
1196        A corresponding IDE instance.
1197    """
1198    if is_mac:
1199        return _get_mac_ide(installed_path, ide, config_reset)
1200    return _get_linux_ide(installed_path, ide, config_reset)
1201
1202
1203def _get_mac_ide(installed_path=None, ide='j', config_reset=False):
1204    """Get IDE to be launched according to the ide input for OS Mac.
1205
1206    Args:
1207        installed_path: The IDE installed path to be checked.
1208        ide: A key character of IDE to be launched. Default ide='j' is to
1209            launch IntelliJ.
1210        config_reset: A boolean, if true reset configuration data.
1211
1212    Returns:
1213        A corresponding IDE instance.
1214    """
1215    if ide == 'e':
1216        return IdeMacEclipse(installed_path, config_reset)
1217    if ide == 's':
1218        return IdeMacStudio(installed_path, config_reset)
1219    if ide == 'c':
1220        return IdeMacCLion(installed_path, config_reset)
1221    if ide == 'v':
1222        return IdeMacVSCode(installed_path, config_reset)
1223    return IdeMacIntelliJ(installed_path, config_reset)
1224
1225
1226def _get_linux_ide(installed_path=None, ide='j', config_reset=False):
1227    """Get IDE to be launched according to the ide input for OS Linux.
1228
1229    Args:
1230        installed_path: The IDE installed path to be checked.
1231        ide: A key character of IDE to be launched. Default ide='j' is to
1232            launch IntelliJ.
1233        config_reset: A boolean, if true reset configuration data.
1234
1235    Returns:
1236        A corresponding IDE instance.
1237    """
1238    if ide == 'e':
1239        return IdeLinuxEclipse(installed_path, config_reset)
1240    if ide == 's':
1241        return IdeLinuxStudio(installed_path, config_reset)
1242    if ide == 'c':
1243        return IdeLinuxCLion(installed_path, config_reset)
1244    if ide == 'v':
1245        return IdeLinuxVSCode(installed_path, config_reset)
1246    return IdeLinuxIntelliJ(installed_path, config_reset)
1247