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