#!/usr/bin/env python3 # # Copyright 2018 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """It is an AIDEGen sub task : IDE operation task! Takes a project file path as input, after passing the needed check(file existence, IDE type, etc.), launch the project in related IDE. Typical usage example: ide_util_obj = IdeUtil() if ide_util_obj.is_ide_installed(): ide_util_obj.config_ide(project_file) ide_util_obj.launch_ide() # Get the configuration folders of IntelliJ or Android Studio. ide_util_obj.get_ide_config_folders() """ import glob import logging import os import platform import re import subprocess from xml.etree import ElementTree from aidegen import constant from aidegen import templates from aidegen.lib import aidegen_metrics from aidegen.lib import android_dev_os from aidegen.lib import common_util from aidegen.lib import config from aidegen.lib import errors from aidegen.lib import ide_common_util from aidegen.lib import project_config from aidegen.lib import project_file_gen from aidegen.sdk import jdk_table from aidegen.lib import xml_util # Add 'nohup' to prevent IDE from being terminated when console is terminated. _IDEA_FOLDER = '.idea' _IML_EXTENSION = '.iml' _JDK_PATH_TOKEN = '@JDKpath' _COMPONENT_END_TAG = ' ' _ECLIPSE_WS = '~/Documents/AIDEGen_Eclipse_workspace' _ALERT_CREATE_WS = ('AIDEGen will create a workspace at %s for Eclipse, ' 'Enter `y` to allow AIDEgen to automatically create the ' 'workspace for you. Otherwise, you need to select the ' 'workspace after Eclipse is launched.\nWould you like ' 'AIDEgen to automatically create the workspace for you?' '(y/n)' % constant.ECLIPSE_WS) _NO_LAUNCH_IDE_CMD = """ Can not find IDE: {}, in path: {}, you can: - add IDE executable to your $PATH or - specify the exact IDE executable path by "aidegen -p" or - specify "aidegen -n" to generate project file only """ _INFO_IMPORT_CONFIG = ('{} needs to import the application configuration for ' 'the new version!\nAfter the import is finished, rerun ' 'the command if your project did not launch. Please ' 'follow the showing dialog to finish the import action.' '\n\n') CONFIG_DIR = 'config' LINUX_JDK_PATH = os.path.join(common_util.get_android_root_dir(), 'prebuilts/jdk/jdk8/linux-x86') LINUX_JDK_TABLE_PATH = 'config/options/jdk.table.xml' LINUX_FILE_TYPE_PATH = 'config/options/filetypes.xml' LINUX_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Android/Sdk') MAC_JDK_PATH = os.path.join(common_util.get_android_root_dir(), 'prebuilts/jdk/jdk8/darwin-x86') ALTERNAIVE_JDK_TABLE_PATH = 'options/jdk.table.xml' ALTERNAIVE_FILE_TYPE_XML_PATH = 'options/filetypes.xml' MAC_ANDROID_SDK_PATH = os.path.join(os.getenv('HOME'), 'Library/Android/sdk') PATTERN_KEY = 'pattern' TYPE_KEY = 'type' _TEST_MAPPING_FILE_TYPE = 'JSON' TEST_MAPPING_NAME = 'TEST_MAPPING' _TEST_MAPPING_TYPE = '' _XPATH_EXTENSION_MAP = 'component/extensionMap' _XPATH_MAPPING = _XPATH_EXTENSION_MAP + '/mapping' _SPECIFIC_INTELLIJ_VERSION = 2020.1 _TEST_MAPPING_FILE_TYPE_ADDING_WARN = '\n{} {}\n'.format( common_util.COLORED_INFO('WARNING:'), ('TEST_MAPPING file type can\'t be added to filetypes.xml. The reason ' 'might be: lack of the parent tag to add TEST_MAPPING file type.')) # pylint: disable=too-many-lines # pylint: disable=invalid-name class IdeUtil: """Provide a set of IDE operations, e.g., launch and configuration. Attributes: _ide: IdeBase derived instance, the related IDE object. For example: 1. Check if IDE is installed. 2. Config IDE, e.g. config code style, SDK path, and etc. 3. Launch an IDE. """ def __init__(self, installed_path=None, ide='j', config_reset=False, is_mac=False): logging.debug('IdeUtil with OS name: %s%s', platform.system(), '(Mac)' if is_mac else '') self._ide = _get_ide(installed_path, ide, config_reset, is_mac) def is_ide_installed(self): """Checks if the IDE is already installed. Returns: True if IDE is installed already, otherwise False. """ return self._ide.is_ide_installed() def launch_ide(self): """Launches the relative IDE by opening the passed project file.""" return self._ide.launch_ide() def config_ide(self, project_abspath): """To config the IDE, e.g., setup code style, init SDK, and etc. Args: project_abspath: An absolute path of the project. """ self._ide.project_abspath = project_abspath if self.is_ide_installed() and self._ide: self._ide.apply_optional_config() def get_default_path(self): """Gets IDE default installed path.""" return self._ide.default_installed_path def ide_name(self): """Gets IDE name.""" return self._ide.ide_name def get_ide_config_folders(self): """Gets the config folders of IDE.""" return self._ide.config_folders class IdeBase: """The most base class of IDE, provides interface and partial path init. Class Attributes: _JDK_PATH: The path of JDK in android project. _IDE_JDK_TABLE_PATH: The path of JDK table which record JDK info in IDE. _IDE_FILE_TYPE_PATH: The path of filetypes.xml. _JDK_CONTENT: A string, the content of the JDK configuration. _DEFAULT_ANDROID_SDK_PATH: A string, the path of Android SDK. _CONFIG_DIR: A string of the config folder name. _SYMBOLIC_VERSIONS: A string list of the symbolic link paths of the relevant IDE. Attributes: _installed_path: String for the IDE binary path. _config_reset: Boolean, True for reset configuration, else not reset. _bin_file_name: String for IDE executable file name. _bin_paths: A list of all possible IDE executable file absolute paths. _ide_name: String for IDE name. _bin_folders: A list of all possible IDE installed paths. config_folders: A list of all possible paths for the IntelliJ configuration folder. project_abspath: The absolute path of the project. For example: 1. Check if IDE is installed. 2. Launch IDE. 3. Config IDE. """ _JDK_PATH = '' _IDE_JDK_TABLE_PATH = '' _IDE_FILE_TYPE_PATH = '' _JDK_CONTENT = '' _DEFAULT_ANDROID_SDK_PATH = '' _CONFIG_DIR = '' _SYMBOLIC_VERSIONS = [] def __init__(self, installed_path=None, config_reset=False): self._installed_path = installed_path self._config_reset = config_reset self._ide_name = '' self._bin_file_name = '' self._bin_paths = [] self._bin_folders = [] self.config_folders = [] self.project_abspath = '' def is_ide_installed(self): """Checks if IDE is already installed. Returns: True if IDE is installed already, otherwise False. """ return bool(self._installed_path) def launch_ide(self): """Launches IDE by opening the passed project file.""" ide_common_util.launch_ide(self.project_abspath, self._get_ide_cmd(), self._ide_name) def apply_optional_config(self): """Do IDEA global config action. Run code style config, SDK config. """ if not self._installed_path: return # Skip config action if there's no config folder exists. _path_list = self._get_config_root_paths() if not _path_list: return self.config_folders = _path_list.copy() for _config_path in _path_list: jdk_file = os.path.join(_config_path, self._IDE_JDK_TABLE_PATH) jdk_xml = jdk_table.JDKTableXML(jdk_file, self._JDK_CONTENT, self._JDK_PATH, self._DEFAULT_ANDROID_SDK_PATH) if jdk_xml.config_jdk_table_xml(): project_file_gen.gen_enable_debugger_module( self.project_abspath, jdk_xml.android_sdk_version) # Set the max file size in the idea.properties. intellij_config_dir = os.path.join(_config_path, self._CONFIG_DIR) config.IdeaProperties(intellij_config_dir).set_max_file_size() self._add_test_mapping_file_type(_config_path) def _add_test_mapping_file_type(self, _config_path): """Adds TEST_MAPPING file type. IntelliJ can't recognize TEST_MAPPING files as the json file. It needs adding file type mapping in filetypes.xml to recognize TEST_MAPPING files. Args: _config_path: the path of IDE config. """ file_type_path = os.path.join(_config_path, self._IDE_FILE_TYPE_PATH) if not os.path.isfile(file_type_path): logging.warning('The file: filetypes.xml is not found.') return file_type_xml = xml_util.parse_xml(file_type_path) if not file_type_xml: logging.warning('Can\'t parse filetypes.xml.') return root = file_type_xml.getroot() add_pattern = True for mapping in root.findall(_XPATH_MAPPING): attrib = mapping.attrib if PATTERN_KEY in attrib and TYPE_KEY in attrib: if attrib[PATTERN_KEY] == TEST_MAPPING_NAME: if attrib[TYPE_KEY] != _TEST_MAPPING_FILE_TYPE: attrib[TYPE_KEY] = _TEST_MAPPING_FILE_TYPE file_type_xml.write(file_type_path) add_pattern = False break if add_pattern: ext_attrib = root.find(_XPATH_EXTENSION_MAP) if not ext_attrib: print(_TEST_MAPPING_FILE_TYPE_ADDING_WARN) return ext_attrib.append(ElementTree.fromstring(_TEST_MAPPING_TYPE)) pretty_xml = common_util.to_pretty_xml(root) common_util.file_generate(file_type_path, pretty_xml) def _get_config_root_paths(self): """Get the config root paths from derived class. Returns: A string list of IDE config paths, return multiple paths if more than one path are found, return an empty list when none is found. """ raise NotImplementedError() @property def default_installed_path(self): """Gets IDE default installed path.""" return ' '.join(self._bin_folders) @property def ide_name(self): """Gets IDE name.""" return self._ide_name def _get_ide_cmd(self): """Compose launch IDE command to run a new process and redirect output. Returns: A string of launch IDE command. """ return ide_common_util.get_run_ide_cmd(self._installed_path, self.project_abspath) def _init_installed_path(self, installed_path): """Initialize IDE installed path. Args: installed_path: the installed path to be checked. """ if installed_path: path_list = ide_common_util.get_script_from_input_path( installed_path, self._bin_file_name) self._installed_path = path_list[0] if path_list else None else: self._installed_path = self._get_script_from_system() if not self._installed_path: logging.error('No %s installed.', self._ide_name) return self._set_installed_path() def _get_script_from_system(self): """Get one IDE installed path from internal path. Returns: The sh full path, or None if not found. """ sh_list = self._get_existent_scripts_in_system() return sh_list[0] if sh_list else None def _get_possible_bin_paths(self): """Gets all possible IDE installed paths.""" return [os.path.join(f, self._bin_file_name) for f in self._bin_folders] def _get_ide_from_environment_paths(self): """Get IDE executable binary file from environment paths. Returns: A string of IDE executable binary path if found, otherwise return None. """ env_paths = os.environ['PATH'].split(':') for env_path in env_paths: path = ide_common_util.get_scripts_from_dir_path( env_path, self._bin_file_name) if path: return path return None def _setup_ide(self): """The callback used to run the necessary setup work of the IDE. When ide_util.config_ide is called to set up the JDK, SDK and some features, the main thread will callback the Idexxx._setup_ide to provide the chance for running the necessary setup of the specific IDE. Default is to do nothing. """ def _get_existent_scripts_in_system(self): """Gets the relevant IDE run script path from system. First get correct IDE installed path from internal paths, if not found search it from environment paths. Returns: The list of script full path, or None if no found. """ return (ide_common_util.get_script_from_internal_path(self._bin_paths, self._ide_name) or self._get_ide_from_environment_paths()) def _get_user_preference(self, versions): """Make sure the version is valid and update preference if needed. Args: versions: A list of the IDE script path, contains the symbolic path. Returns: An IDE script path, or None is not found. """ if not versions: return None if len(versions) == 1: return versions[0] with config.AidegenConfig() as conf: if not self._config_reset and (conf.preferred_version(self.ide_name) in versions): return conf.preferred_version(self.ide_name) display_versions = self._merge_symbolic_version(versions) preferred = ide_common_util.ask_preference(display_versions, self.ide_name) if preferred: conf.set_preferred_version(self._get_real_path(preferred), self.ide_name) return conf.preferred_version(self.ide_name) def _set_installed_path(self): """Write the user's input installed path into the config file. If users input an existent IDE installed path, we should keep it in the configuration. """ if self._installed_path: with config.AidegenConfig() as aconf: aconf.set_preferred_version(self._installed_path, self.ide_name) def _merge_symbolic_version(self, versions): """Merge the duplicate version of symbolic links. Stable and beta versions are a symbolic link to an existing version. This function assemble symbolic and real to make it more clear to read. Ex: ['/opt/intellij-ce-stable/bin/idea.sh', '/opt/intellij-ce-2019.1/bin/idea.sh'] to ['/opt/intellij-ce-stable/bin/idea.sh -> /opt/intellij-ce-2019.1/bin/idea.sh', '/opt/intellij-ce-2019.1/bin/idea.sh'] Args: versions: A list of all installed versions. Returns: A list of versions to show for user to select. It may contain 'symbolic_path/idea.sh -> original_path/idea.sh'. """ display_versions = versions[:] for symbolic in self._SYMBOLIC_VERSIONS: if symbolic in display_versions and (os.path.isfile(symbolic)): real_path = os.path.realpath(symbolic) for index, path in enumerate(display_versions): if path == symbolic: display_versions[index] = ' -> '.join( [display_versions[index], real_path]) break return display_versions @staticmethod def _get_real_path(path): """ Get real path from merged path. Turn the path string "/opt/intellij-ce-stable/bin/idea.sh -> /opt/ intellij-ce-2019.2/bin/idea.sh" into "/opt/intellij-ce-stable/bin/idea.sh" Args: path: A path string may be merged with symbolic path. Returns: The real IntelliJ installed path. """ return path.split()[0] class IdeIntelliJ(IdeBase): """Provide basic IntelliJ ops, e.g., launch IDEA, and config IntelliJ. For example: 1. Check if IntelliJ is installed. 2. Launch an IntelliJ. 3. Config IntelliJ. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._ide_name = constant.IDE_INTELLIJ self._ls_ce_path = '' self._ls_ue_path = '' def _get_config_root_paths(self): """Get the config root paths from derived class. Returns: A string list of IDE config paths, return multiple paths if more than one path are found, return an empty list when none is found. """ raise NotImplementedError() def _get_preferred_version(self): """Get the user's preferred IntelliJ version. Locates the IntelliJ IDEA launch script path by following rule. 1. If config file recorded user's preference version, load it. 2. If config file didn't record, search them form default path if there are more than one version, ask user and record it. Returns: The sh full path, or None if no IntelliJ version is installed. """ ce_paths = ide_common_util.get_intellij_version_path(self._ls_ce_path) ue_paths = ide_common_util.get_intellij_version_path(self._ls_ue_path) all_versions = self._get_all_versions(ce_paths, ue_paths) tmp_versions = all_versions.copy() for version in tmp_versions: real_version = os.path.realpath(version) if config.AidegenConfig.deprecated_intellij_version(real_version): all_versions.remove(version) return self._get_user_preference(all_versions) def _setup_ide(self): """The callback used to run the necessary setup work for the IDE. IntelliJ has a default flow to let the user import the configuration from the previous version, aidegen makes sure not to break the behavior by checking in this callback implementation. """ run_script_path = os.path.realpath(self._installed_path) app_folder = self._get_application_path(run_script_path) if not app_folder: logging.warning('\nInvalid IDE installed path.') return show_hint = False ide_version = self._get_ide_version(app_folder) folder_path = self._get_config_dir(ide_version, app_folder) import_process = None while not os.path.isdir(folder_path): # Guide the user to go through the IDE flow. if not show_hint: print('\n{} {}'.format(common_util.COLORED_INFO('INFO:'), _INFO_IMPORT_CONFIG.format( self.ide_name))) try: import_process = subprocess.Popen( ide_common_util.get_run_ide_cmd(run_script_path, '', False), shell=True) except (subprocess.SubprocessError, ValueError): logging.warning('\nSubprocess call gets the invalid input.') finally: show_hint = True if import_process: try: import_process.wait(1) except subprocess.TimeoutExpired: import_process.terminate() return def _get_script_from_system(self): """Get correct IntelliJ installed path from internal path. Returns: The sh full path, or None if no IntelliJ version is installed. """ found = self._get_preferred_version() if found: logging.debug('IDE internal installed path: %s.', found) return found @staticmethod def _get_all_versions(cefiles, uefiles): """Get all versions of launch script files. Args: cefiles: CE version launch script paths. uefiles: UE version launch script paths. Returns: A list contains all versions of launch script files. """ all_versions = [] if cefiles: all_versions.extend(cefiles) if uefiles: all_versions.extend(uefiles) return all_versions @staticmethod def _get_application_path(run_script_path): """Get the relevant configuration folder based on the run script path. Args: run_script_path: The string of the run script path for the IntelliJ. Returns: The string of the IntelliJ application folder name or None if the run_script_path is invalid. The returned folder format is as follows, 1. .IdeaIC2019.3 2. .IntelliJIdea2019.3 3. IntelliJIdea2020.1 """ if not run_script_path or not os.path.isfile(run_script_path): return None index = str.find(run_script_path, 'intellij-') target_path = None if index == -1 else run_script_path[index:] if not target_path or '-' not in run_script_path: return None return IdeIntelliJ._get_config_folder_name(target_path) @staticmethod def _get_ide_version(config_folder_name): """Gets IntelliJ version from the input app folder name. Args: config_folder_name: A string of the app folder name. Returns: A string of the IntelliJ version. """ versions = re.findall(r'\d+', config_folder_name) if not versions: logging.warning('\nInvalid IntelliJ config folder name: %s.', config_folder_name) return None return '.'.join(versions) @staticmethod def _get_config_folder_name(script_folder_name): """Gets IntelliJ config folder name from the IDE version. The config folder name has been changed since 2020.1. Args: script_folder_name: A string of the script folder name of IntelliJ. Returns: A string of the IntelliJ config folder name. """ path_data = script_folder_name.split('-') if not path_data or len(path_data) < 3: return None ide_version = path_data[2].split(os.sep)[0] numbers = ide_version.split('.') if len(numbers) > 2: ide_version = '.'.join([numbers[0], numbers[1]]) try: version = float(ide_version) except ValueError: return None pre_folder = '.IdeaIC' if version < _SPECIFIC_INTELLIJ_VERSION: if path_data[1] == 'ue': pre_folder = '.IntelliJIdea' else: if path_data[1] == 'ce': pre_folder = 'IdeaIC' elif path_data[1] == 'ue': pre_folder = 'IntelliJIdea' return ''.join([pre_folder, ide_version]) @staticmethod def _get_config_dir(ide_version, config_folder_name): """Gets IntelliJ config directory by the config folder name. The IntelliJ config directory is changed from version 2020.1. Get the version from app folder name and determine the config directory. URL: https://intellij-support.jetbrains.com/hc/en-us/articles/206544519 Args: ide_version: A string of the IntelliJ's version. config_folder_name: A string of the IntelliJ's config folder name. Returns: A string of the IntelliJ config directory. """ try: version = float(ide_version) except ValueError: return None if version < _SPECIFIC_INTELLIJ_VERSION: return os.path.join( os.getenv('HOME'), config_folder_name) return os.path.join( os.getenv('HOME'), '.config', 'JetBrains', config_folder_name) class IdeLinuxIntelliJ(IdeIntelliJ): """Provide the IDEA behavior implementation for OS Linux. Class Attributes: _INTELLIJ_RE: Regular expression of IntelliJ installed name in GLinux. For example: 1. Check if IntelliJ is installed. 2. Launch an IntelliJ. 3. Config IntelliJ. """ _JDK_PATH = LINUX_JDK_PATH # TODO(b/127899277): Preserve a config for jdk version option case. _CONFIG_DIR = CONFIG_DIR _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH _IDE_FILE_TYPE_PATH = LINUX_FILE_TYPE_PATH _JDK_CONTENT = templates.LINUX_JDK_XML _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH _SYMBOLIC_VERSIONS = ['/opt/intellij-ce-stable/bin/idea.sh', '/opt/intellij-ue-stable/bin/idea.sh', '/opt/intellij-ce-beta/bin/idea.sh', '/opt/intellij-ue-beta/bin/idea.sh'] _INTELLIJ_RE = re.compile(r'intellij-(ce|ue)-') def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'idea.sh' self._bin_folders = ['/opt/intellij-*/bin'] self._ls_ce_path = os.path.join('/opt/intellij-ce-*/bin', self._bin_file_name) self._ls_ue_path = os.path.join('/opt/intellij-ue-*/bin', self._bin_file_name) self._init_installed_path(installed_path) def _get_config_root_paths(self): """To collect the global config folder paths of IDEA as a string list. The config folder of IntelliJ IDEA is under the user's home directory, .IdeaIC20xx.x and .IntelliJIdea20xx.x are folder names for different versions. Returns: A string list for IDE config root paths, and return an empty list when none is found. """ if not self._installed_path: return None _config_folders = [] _config_folder = '' if IdeLinuxIntelliJ._INTELLIJ_RE.search(self._installed_path): _path_data = os.path.realpath(self._installed_path) _config_folder = self._get_application_path(_path_data) if not _config_folder: return None ide_version = self._get_ide_version(_config_folder) if not ide_version: return None try: version = float(ide_version) except ValueError: return None folder_path = self._get_config_dir(ide_version, _config_folder) if version >= _SPECIFIC_INTELLIJ_VERSION: self._IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH self._IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH if not os.path.isdir(folder_path): logging.debug("\nThe config folder: %s doesn't exist", _config_folder) self._setup_ide() _config_folders.append(folder_path) else: # TODO(b/123459239): For the case that the user provides the IDEA # binary path, we now collect all possible IDEA config root paths. _config_folders = glob.glob( os.path.join(os.getenv('HOME'), '.IdeaI?20*')) _config_folders.extend( glob.glob(os.path.join(os.getenv('HOME'), '.IntelliJIdea20*'))) _config_folders.extend( glob.glob(os.path.join(os.getenv('HOME'), '.config', 'IntelliJIdea202*'))) logging.debug('The config path list: %s.', _config_folders) return _config_folders class IdeMacIntelliJ(IdeIntelliJ): """Provide the IDEA behavior implementation for OS Mac. For example: 1. Check if IntelliJ is installed. 2. Launch an IntelliJ. 3. Config IntelliJ. """ _JDK_PATH = MAC_JDK_PATH _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH _IDE_FILE_TYPE_PATH = ALTERNAIVE_FILE_TYPE_XML_PATH _JDK_CONTENT = templates.MAC_JDK_XML _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'idea' self._bin_folders = ['/Applications/IntelliJ IDEA.app/Contents/MacOS'] self._bin_paths = self._get_possible_bin_paths() self._ls_ce_path = os.path.join( '/Applications/IntelliJ IDEA CE.app/Contents/MacOS', self._bin_file_name) self._ls_ue_path = os.path.join( '/Applications/IntelliJ IDEA.app/Contents/MacOS', self._bin_file_name) self._init_installed_path(installed_path) def _get_config_root_paths(self): """To collect the global config folder paths of IDEA as a string list. Returns: A string list for IDE config root paths, and return an empty list when none is found. """ if not self._installed_path: return None _config_folders = [] if 'IntelliJ' in self._installed_path: _config_folders = glob.glob( os.path.join( os.getenv('HOME'), 'Library/Preferences/IdeaI?20*')) _config_folders.extend( glob.glob( os.path.join( os.getenv('HOME'), 'Library/Preferences/IntelliJIdea20*'))) return _config_folders class IdeStudio(IdeBase): """Class offers a set of Android Studio launching utilities. For example: 1. Check if Android Studio is installed. 2. Launch an Android Studio. 3. Config Android Studio. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._ide_name = constant.IDE_ANDROID_STUDIO def _get_config_root_paths(self): """Get the config root paths from derived class. Returns: A string list of IDE config paths, return multiple paths if more than one path are found, return an empty list when none is found. """ raise NotImplementedError() def _get_script_from_system(self): """Get correct Studio installed path from internal path. Returns: The sh full path, or None if no Studio version is installed. """ found = self._get_preferred_version() if found: logging.debug('IDE internal installed path: %s.', found) return found def _get_preferred_version(self): """Get the user's preferred Studio version. Locates the Studio launch script path by following rule. 1. If config file recorded user's preference version, load it. 2. If config file didn't record, search them form default path if there are more than one version, ask user and record it. Returns: The sh full path, or None if no Studio version is installed. """ versions = self._get_existent_scripts_in_system() if not versions: return None for version in versions: real_version = os.path.realpath(version) if config.AidegenConfig.deprecated_studio_version(real_version): versions.remove(version) return self._get_user_preference(versions) def apply_optional_config(self): """Do the configuration of Android Studio. Configures code style and SDK for Java project and do nothing for others. """ if not self.project_abspath: return # TODO(b/150662865): The following workaround should be replaced. # Since the path of the artifact for Java is the .idea directory but # native is a CMakeLists.txt file using this to workaround first. if os.path.isfile(self.project_abspath): return if os.path.isdir(self.project_abspath): IdeBase.apply_optional_config(self) class IdeLinuxStudio(IdeStudio): """Class offers a set of Android Studio launching utilities for OS Linux. For example: 1. Check if Android Studio is installed. 2. Launch an Android Studio. 3. Config Android Studio. """ _JDK_PATH = LINUX_JDK_PATH _CONFIG_DIR = CONFIG_DIR _IDE_JDK_TABLE_PATH = LINUX_JDK_TABLE_PATH _JDK_CONTENT = templates.LINUX_JDK_XML _DEFAULT_ANDROID_SDK_PATH = LINUX_ANDROID_SDK_PATH _SYMBOLIC_VERSIONS = [ '/opt/android-studio-with-blaze-stable/bin/studio.sh', '/opt/android-studio-stable/bin/studio.sh', '/opt/android-studio-with-blaze-beta/bin/studio.sh', '/opt/android-studio-beta/bin/studio.sh'] def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'studio.sh' self._bin_folders = ['/opt/android-studio*/bin'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) def _get_config_root_paths(self): """Collect the global config folder paths as a string list. Returns: A string list for IDE config root paths, and return an empty list when none is found. """ return glob.glob(os.path.join(os.getenv('HOME'), '.AndroidStudio*')) class IdeMacStudio(IdeStudio): """Class offers a set of Android Studio launching utilities for OS Mac. For example: 1. Check if Android Studio is installed. 2. Launch an Android Studio. 3. Config Android Studio. """ _JDK_PATH = MAC_JDK_PATH _IDE_JDK_TABLE_PATH = ALTERNAIVE_JDK_TABLE_PATH _JDK_CONTENT = templates.MAC_JDK_XML _DEFAULT_ANDROID_SDK_PATH = MAC_ANDROID_SDK_PATH def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'studio' self._bin_folders = ['/Applications/Android Studio.app/Contents/MacOS'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) def _get_config_root_paths(self): """Collect the global config folder paths as a string list. Returns: A string list for IDE config root paths, and return an empty list when none is found. """ return glob.glob( os.path.join( os.getenv('HOME'), 'Library/Preferences/AndroidStudio*')) class IdeEclipse(IdeBase): """Class offers a set of Eclipse launching utilities. Attributes: cmd: A list of the build command. For example: 1. Check if Eclipse is installed. 2. Launch an Eclipse. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._ide_name = constant.IDE_ECLIPSE self._bin_file_name = 'eclipse' self.cmd = [] def _get_script_from_system(self): """Get correct IDE installed path from internal path. Remove any file with extension, the filename should be like, 'eclipse', 'eclipse47' and so on, check if the file is executable and filter out file such as 'eclipse.ini'. Returns: The sh full path, or None if no IntelliJ version is installed. """ for ide_path in self._bin_paths: # The binary name of Eclipse could be eclipse47, eclipse49, # eclipse47_testing or eclipse49_testing. So finding the matched # binary by /path/to/ide/eclipse*. ls_output = glob.glob(ide_path + '*', recursive=True) if ls_output: ls_output = sorted(ls_output) match_eclipses = [] for path in ls_output: if os.access(path, os.X_OK): match_eclipses.append(path) if match_eclipses: match_eclipses = sorted(match_eclipses) logging.debug('Result for checking %s after sort: %s.', self._ide_name, match_eclipses[0]) return match_eclipses[0] return None def _get_ide_cmd(self): """Compose launch IDE command to run a new process and redirect output. AIDEGen will create a default workspace ~/Documents/AIDEGen_Eclipse_workspace for users if they agree to do that. Also, we could not import the default project through the command line so remove the project path argument. Returns: A string of launch IDE command. """ if (os.path.exists(os.path.expanduser(constant.ECLIPSE_WS)) or str(input(_ALERT_CREATE_WS)).lower() == 'y'): self.cmd.extend(['-data', constant.ECLIPSE_WS]) self.cmd.extend([constant.IGNORE_STD_OUT_ERR_CMD, '&']) return ' '.join(self.cmd) def apply_optional_config(self): """Override to do nothing.""" def _get_config_root_paths(self): """Override to do nothing.""" class IdeLinuxEclipse(IdeEclipse): """Class offers a set of Eclipse launching utilities for OS Linux. For example: 1. Check if Eclipse is installed. 2. Launch an Eclipse. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_folders = ['/opt/eclipse*', '/usr/bin/'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) self.cmd = [constant.NOHUP, self._installed_path.replace(' ', r'\ ')] class IdeMacEclipse(IdeEclipse): """Class offers a set of Eclipse launching utilities for OS Mac. For example: 1. Check if Eclipse is installed. 2. Launch an Eclipse. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'eclipse' self._bin_folders = [os.path.expanduser('~/eclipse/**')] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) self.cmd = [self._installed_path.replace(' ', r'\ ')] class IdeCLion(IdeBase): """Class offers a set of CLion launching utilities. For example: 1. Check if CLion is installed. 2. Launch an CLion. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._ide_name = constant.IDE_CLION def apply_optional_config(self): """Override to do nothing.""" def _get_config_root_paths(self): """Override to do nothing.""" class IdeLinuxCLion(IdeCLion): """Class offers a set of CLion launching utilities for OS Linux. For example: 1. Check if CLion is installed. 2. Launch an CLion. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'clion.sh' # TODO(b/141288011): Handle /opt/clion-*/bin to let users choose a # preferred version of CLion in the future. self._bin_folders = ['/opt/clion-stable/bin'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) class IdeMacCLion(IdeCLion): """Class offers a set of Android Studio launching utilities for OS Mac. For example: 1. Check if Android Studio is installed. 2. Launch an Android Studio. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'clion' self._bin_folders = ['/Applications/CLion.app/Contents/MacOS/CLion'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) class IdeVSCode(IdeBase): """Class offers a set of VSCode launching utilities. For example: 1. Check if VSCode is installed. 2. Launch an VSCode. """ def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._ide_name = constant.IDE_VSCODE def apply_optional_config(self): """Override to do nothing.""" def _get_config_root_paths(self): """Override to do nothing.""" class IdeLinuxVSCode(IdeVSCode): """Class offers a set of VSCode launching utilities for OS Linux.""" def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'code' self._bin_folders = ['/usr/bin'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) class IdeMacVSCode(IdeVSCode): """Class offers a set of VSCode launching utilities for OS Mac.""" def __init__(self, installed_path=None, config_reset=False): super().__init__(installed_path, config_reset) self._bin_file_name = 'code' self._bin_folders = ['/usr/local/bin'] self._bin_paths = self._get_possible_bin_paths() self._init_installed_path(installed_path) def get_ide_util_instance(ide='j'): """Get an IdeUtil class instance for launching IDE. Args: ide: A key character of IDE to be launched. Default ide='j' is to launch IntelliJ. Returns: An IdeUtil class instance. """ conf = project_config.ProjectConfig.get_instance() if not conf.is_launch_ide: return None is_mac = (android_dev_os.AndroidDevOS.MAC == android_dev_os.AndroidDevOS. get_os_type()) tool = IdeUtil(conf.ide_installed_path, ide, conf.config_reset, is_mac) if not tool.is_ide_installed(): ipath = conf.ide_installed_path or tool.get_default_path() err = _NO_LAUNCH_IDE_CMD.format(constant.IDE_NAME_DICT[ide], ipath) logging.error(err) stack_trace = common_util.remove_user_home_path(err) logs = '%s exists? %s' % (common_util.remove_user_home_path(ipath), os.path.exists(ipath)) aidegen_metrics.ends_asuite_metrics(constant.IDE_LAUNCH_FAILURE, stack_trace, logs) raise errors.IDENotExistError(err) return tool def _get_ide(installed_path=None, ide='j', config_reset=False, is_mac=False): """Get IDE to be launched according to the ide input and OS type. Args: installed_path: The IDE installed path to be checked. ide: A key character of IDE to be launched. Default ide='j' is to launch IntelliJ. config_reset: A boolean, if true reset configuration data. Returns: A corresponding IDE instance. """ if is_mac: return _get_mac_ide(installed_path, ide, config_reset) return _get_linux_ide(installed_path, ide, config_reset) def _get_mac_ide(installed_path=None, ide='j', config_reset=False): """Get IDE to be launched according to the ide input for OS Mac. Args: installed_path: The IDE installed path to be checked. ide: A key character of IDE to be launched. Default ide='j' is to launch IntelliJ. config_reset: A boolean, if true reset configuration data. Returns: A corresponding IDE instance. """ if ide == 'e': return IdeMacEclipse(installed_path, config_reset) if ide == 's': return IdeMacStudio(installed_path, config_reset) if ide == 'c': return IdeMacCLion(installed_path, config_reset) if ide == 'v': return IdeMacVSCode(installed_path, config_reset) return IdeMacIntelliJ(installed_path, config_reset) def _get_linux_ide(installed_path=None, ide='j', config_reset=False): """Get IDE to be launched according to the ide input for OS Linux. Args: installed_path: The IDE installed path to be checked. ide: A key character of IDE to be launched. Default ide='j' is to launch IntelliJ. config_reset: A boolean, if true reset configuration data. Returns: A corresponding IDE instance. """ if ide == 'e': return IdeLinuxEclipse(installed_path, config_reset) if ide == 's': return IdeLinuxStudio(installed_path, config_reset) if ide == 'c': return IdeLinuxCLion(installed_path, config_reset) if ide == 'v': return IdeLinuxVSCode(installed_path, config_reset) return IdeLinuxIntelliJ(installed_path, config_reset)