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