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