1#!/usr/bin/env python3 2# 3# Copyright 2020 - 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"""Configs the jdk.table.xml. 18 19In order to enable the feature "Attach debugger to Android process" in Android 20Studio or IntelliJ, AIDEGen needs the JDK and Android SDK been set-up. The class 21JDKTableXML parses the jdk.table.xml to find the existing JDK and Android SDK 22information. If they do not exist, AIDEGen will create them. 23 24 Usage example: 25 jdk_table_xml = JDKTableXML(jdk_table_xml_file, 26 jdk_template, 27 default_jdk_path, 28 default_android_sdk_path) 29 if jdk_table_xml.config_jdk_table_xml(): 30 android_sdk_version = jdk_table_xml.android_sdk_version 31""" 32 33from __future__ import absolute_import 34 35import os 36 37from xml.etree import ElementTree 38 39from aidegen import constant 40from aidegen import templates 41from aidegen.lib import aidegen_metrics 42from aidegen.lib import common_util 43from aidegen.lib import xml_util 44from aidegen.sdk import android_sdk 45 46 47class JDKTableXML(): 48 """Configs jdk.table.xml for IntelliJ and Android Studio. 49 50 Attributes: 51 _config_file: The absolute file path of the jdk.table.xml, the file 52 might not exist. 53 _jdk_content: A string, the content of the JDK configuration. 54 _jdk_path: The path of JDK in android project. 55 _default_android_sdk_path: The default path to the Android SDK. 56 _platform_version: The version name of the platform, e.g. android-29 57 _android_sdk_version: The version name of the Android SDK in the 58 jdk.table.xml, e.g. Android API 29 Platform 59 _modify_config: A boolean, True to write new content to jdk.table.xml. 60 _xml: An xml.etree.ElementTree object contains the XML parsing result. 61 _sdk: An AndroidSDK object to get the Android SDK path and platform 62 mapping. 63 """ 64 _JDK = 'jdk' 65 _NAME = 'name' 66 _TYPE = 'type' 67 _VALUE = 'value' 68 _SDK = 'sdk' 69 _HOMEPATH = 'homePath' 70 _ADDITIONAL = 'additional' 71 _ANDROID_SDK = 'Android SDK' 72 _JAVA_SDK = 'JavaSDK' 73 _JDK_VERSION = 'JDK18' 74 _APPLICATION = 'application' 75 _COMPONENT = 'component' 76 _PROJECTJDKTABLE = 'ProjectJdkTable' 77 _LAST_TAG_TAIL = '\n ' 78 _NEW_TAG_TAIL = '\n ' 79 _ANDROID_SDK_VERSION = 'Android API {CODE_NAME} Platform' 80 _DEFAULT_JDK_TABLE_XML = os.path.join(common_util.get_android_root_dir(), 81 constant.AIDEGEN_ROOT_PATH, 82 'data', 83 'jdk.table.xml') 84 _ILLEGAL_XML = ('The {XML} is not an useful XML file for IntelliJ. Do you ' 85 'agree AIDEGen override it?(y/n)') 86 _IGNORE_XML_WARNING = ('The {XML} is not an useful XML file for IntelliJ. ' 87 'It causes the feature "Attach debugger to Android ' 88 'process" to be disabled.') 89 90 def __init__(self, config_file, jdk_content, jdk_path, 91 default_android_sdk_path): 92 """JDKTableXML initialize. 93 94 Args: 95 config_file: The absolute file path of the jdk.table.xml, the file 96 might not exist. 97 jdk_content: A string, the content of the JDK configuration. 98 jdk_path: The path of JDK in android project. 99 default_android_sdk_path: The default absolute path to the Android 100 SDK. 101 """ 102 self._config_file = config_file 103 self._jdk_content = jdk_content 104 self._jdk_path = jdk_path 105 self._default_android_sdk_path = default_android_sdk_path 106 self._xml = None 107 if os.path.exists(config_file): 108 xml_file = config_file 109 else: 110 xml_file = self._DEFAULT_JDK_TABLE_XML 111 common_util.file_generate(xml_file, templates.JDK_TABLE_XML) 112 self._xml = xml_util.parse_xml(xml_file) 113 self._platform_version = None 114 self._android_sdk_version = None 115 self._modify_config = False 116 self._sdk = android_sdk.AndroidSDK() 117 118 @property 119 def android_sdk_version(self): 120 """Gets the Android SDK version.""" 121 return self._android_sdk_version 122 123 def _check_structure(self): 124 """Checks the XML's structure is correct. 125 126 The content of the XML file should have a root tag as <application> and 127 a child tag <component> of it. 128 E.g. 129 <application> 130 <component name="ProjectJdkTable"> 131 ... 132 </component> 133 </application> 134 135 Returns: 136 Boolean: True if the structure is correct, otherwise False. 137 """ 138 if (not self._xml or self._xml.getroot().tag != self._APPLICATION 139 or self._xml.find(self._COMPONENT) is None 140 or self._xml.find(self._COMPONENT).tag != self._COMPONENT): 141 return False 142 return self._xml.find(self._COMPONENT).get( 143 self._NAME) == self._PROJECTJDKTABLE 144 145 def _override_xml(self): 146 """Overrides the XML file when it's invalid. 147 148 Returns: 149 A boolean, True when developers choose to override the XML file, 150 otherwise False. 151 """ 152 input_data = input(self._ILLEGAL_XML.format(XML=self._config_file)) 153 while input_data not in ('y', 'n'): 154 input_data = input('Please type y(Yes) or n(No): ') 155 if input_data == 'y': 156 # Record the exception about wrong XML format. 157 if self._xml: 158 aidegen_metrics.send_exception_metrics( 159 constant.XML_PARSING_FAILURE, '', 160 ElementTree.tostring(self._xml.getroot()), '') 161 self._xml = xml_util.parse_xml(self._DEFAULT_JDK_TABLE_XML) 162 return True 163 return False 164 165 def _check_jdk18_in_xml(self): 166 """Checks if the JDK18 is already set in jdk.table.xml. 167 168 Returns: 169 Boolean: True if the JDK18 exists else False. 170 """ 171 for jdk in self._xml.iter(self._JDK): 172 _name = jdk.find(self._NAME) 173 _type = jdk.find(self._TYPE) 174 if None in (_name, _type): 175 continue 176 if (_type.get(self._VALUE) == self._JAVA_SDK 177 and _name.get(self._VALUE) == self._JDK_VERSION): 178 return True 179 return False 180 181 def _check_android_sdk_in_xml(self): 182 """Checks if the Android SDK is already set in jdk.table.xml. 183 184 If the Android SDK exists in xml, validate the value of Android SDK path 185 and platform version. 186 1. Check if the Android SDK path is valid. 187 2. Check if the platform version exists in the Android SDK. 188 The Android SDK version can be used to generate enble_debugger module 189 when condition 1 and 2 are true. 190 191 Returns: 192 Boolean: True if the Android SDK configuration exists, otherwise 193 False. 194 """ 195 for tag in self._xml.iter(self._JDK): 196 _name = tag.find(self._NAME) 197 _type = tag.find(self._TYPE) 198 _homepath = tag.find(self._HOMEPATH) 199 _additional = tag.find(self._ADDITIONAL) 200 if None in (_name, _type, _homepath, _additional): 201 continue 202 203 tag_type = _type.get(self._VALUE) 204 home_path = _homepath.get(self._VALUE).replace( 205 constant.USER_HOME, os.path.expanduser('~')) 206 platform = _additional.get(self._SDK) 207 if (tag_type != self._ANDROID_SDK 208 or not self._sdk.is_android_sdk_path(home_path) 209 or platform not in self._sdk.platform_mapping): 210 continue 211 self._android_sdk_version = _name.get(self._VALUE) 212 self._platform_version = platform 213 return True 214 return False 215 216 def _append_config(self, new_config): 217 """Adds a <jdk> configuration at the last of <component>. 218 219 Args: 220 new_config: A string of new <jdk> configuration. 221 """ 222 node = ElementTree.fromstring(new_config) 223 node.tail = self._NEW_TAG_TAIL 224 component = self._xml.getroot().find(self._COMPONENT) 225 if len(component) > 0: 226 component[-1].tail = self._LAST_TAG_TAIL 227 else: 228 component.text = self._LAST_TAG_TAIL 229 self._xml.getroot().find(self._COMPONENT).append(node) 230 231 def _generate_jdk_config_string(self): 232 """Generates the default JDK configuration.""" 233 if self._check_jdk18_in_xml(): 234 return 235 self._append_config(self._jdk_content.format(JDKpath=self._jdk_path)) 236 self._modify_config = True 237 238 def _generate_sdk_config_string(self): 239 """Generates Android SDK configuration.""" 240 if self._check_android_sdk_in_xml(): 241 return 242 if self._sdk.path_analysis(self._default_android_sdk_path): 243 # TODO(b/151582629): Revise the API_LEVEL to CODE_NAME when 244 # abandoning the sdk_config.py. 245 self._append_config(templates.ANDROID_SDK_XML.format( 246 ANDROID_SDK_PATH=self._sdk.android_sdk_path, 247 CODE_NAME=self._sdk.max_code_name)) 248 self._android_sdk_version = self._ANDROID_SDK_VERSION.format( 249 CODE_NAME=self._sdk.max_code_name) 250 self._modify_config = True 251 return 252 # Record the exception about missing Android SDK. 253 aidegen_metrics.send_exception_metrics( 254 constant.LOCATE_SDK_PATH_FAILURE, '', 255 ElementTree.tostring(self._xml.getroot()), '') 256 257 def config_jdk_table_xml(self): 258 """Configures the jdk.table.xml. 259 260 1. Generate the JDK18 configuration if it does not exist. 261 2. Generate the Android SDK configuration if it does not exist and 262 save the Android SDK path. 263 3. Update the jdk.table.xml if AIDEGen needs to append JDK18 or 264 Android SDK configuration. 265 266 Returns: 267 A boolean, True when get the Android SDK version, otherwise False. 268 """ 269 if not self._check_structure() and not self._override_xml(): 270 print(self._IGNORE_XML_WARNING.format(XML=self._config_file)) 271 return False 272 self._generate_jdk_config_string() 273 self._generate_sdk_config_string() 274 if self._modify_config: 275 if not os.path.exists(self._config_file): 276 common_util.file_generate( 277 self._config_file, templates.JDK_TABLE_XML) 278 self._xml.write(self._config_file) 279 return bool(self._android_sdk_version) 280