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 : generate the project files. 18 19 Usage example: 20 projects: A list of ProjectInfo instances. 21 ProjectFileGenerator.generate_ide_project_file(projects) 22""" 23 24import logging 25import os 26import shutil 27 28from aidegen import constant 29from aidegen import templates 30from aidegen.idea import iml 31from aidegen.idea import xml_gen 32from aidegen.lib import common_util 33from aidegen.lib import config 34from aidegen.lib import project_config 35from aidegen.project import source_splitter 36 37# FACET_SECTION is a part of iml, which defines the framework of the project. 38_MODULE_SECTION = (' <module fileurl="file:///$PROJECT_DIR$/%s.iml"' 39 ' filepath="$PROJECT_DIR$/%s.iml" />') 40_SUB_MODULES_SECTION = (' <module fileurl="file:///{IML}" ' 41 'filepath="{IML}" />') 42_MODULE_TOKEN = '@MODULES@' 43_ENABLE_DEBUGGER_MODULE_TOKEN = '@ENABLE_DEBUGGER_MODULE@' 44_IDEA_FOLDER = '.idea' 45_MODULES_XML = 'modules.xml' 46_COPYRIGHT_FOLDER = 'copyright' 47_CODE_STYLE_FOLDER = 'codeStyles' 48_APACHE_2_XML = 'Apache_2.xml' 49_PROFILES_SETTINGS_XML = 'profiles_settings.xml' 50_CODE_STYLE_CONFIG_XML = 'codeStyleConfig.xml' 51_JSON_SCHEMAS_CONFIG_XML = 'jsonSchemas.xml' 52_PROJECT_XML = 'Project.xml' 53_COMPILE_XML = 'compiler.xml' 54_MISC_XML = 'misc.xml' 55_CONFIG_JSON = 'config.json' 56_GIT_FOLDER_NAME = '.git' 57# Support gitignore by symbolic link to aidegen/data/gitignore_template. 58_GITIGNORE_FILE_NAME = '.gitignore' 59_GITIGNORE_REL_PATH = 'tools/asuite/aidegen/data/gitignore_template' 60_GITIGNORE_ABS_PATH = os.path.join(common_util.get_android_root_dir(), 61 _GITIGNORE_REL_PATH) 62# Support code style by symbolic link to aidegen/data/AndroidStyle_aidegen.xml. 63_CODE_STYLE_REL_PATH = 'tools/asuite/aidegen/data/AndroidStyle_aidegen.xml' 64_CODE_STYLE_SRC_PATH = os.path.join(common_util.get_android_root_dir(), 65 _CODE_STYLE_REL_PATH) 66_TEST_MAPPING_CONFIG_PATH = ('tools/tradefederation/core/src/com/android/' 67 'tradefed/util/testmapping/TEST_MAPPING.config' 68 '.json') 69 70 71class ProjectFileGenerator: 72 """Project file generator. 73 74 Attributes: 75 project_info: A instance of ProjectInfo. 76 """ 77 78 def __init__(self, project_info): 79 """ProjectFileGenerator initialize. 80 81 Args: 82 project_info: A instance of ProjectInfo. 83 """ 84 self.project_info = project_info 85 86 def generate_intellij_project_file(self, iml_path_list=None): 87 """Generates IntelliJ project file. 88 89 # TODO(b/155346505): Move this method to idea folder. 90 91 Args: 92 iml_path_list: An optional list of submodule's iml paths, the 93 default value is None. 94 """ 95 if self.project_info.is_main_project: 96 self._generate_modules_xml(iml_path_list) 97 self._copy_constant_project_files() 98 99 @classmethod 100 def generate_ide_project_files(cls, projects): 101 """Generate IDE project files by a list of ProjectInfo instances. 102 103 It deals with the sources by ProjectSplitter to create iml files for 104 each project and generate_intellij_project_file only creates 105 the other project files under .idea/. 106 107 Args: 108 projects: A list of ProjectInfo instances. 109 """ 110 # Initialization 111 iml.IMLGenerator.USED_NAME_CACHE.clear() 112 proj_splitter = source_splitter.ProjectSplitter(projects) 113 proj_splitter.get_dependencies() 114 proj_splitter.revise_source_folders() 115 iml_paths = [proj_splitter.gen_framework_srcjars_iml()] 116 proj_splitter.gen_projects_iml() 117 iml_paths += [project.iml_path for project in projects] 118 ProjectFileGenerator( 119 projects[0]).generate_intellij_project_file(iml_paths) 120 _merge_project_vcs_xmls(projects) 121 122 def _copy_constant_project_files(self): 123 """Copy project files to target path with error handling. 124 125 This function would copy compiler.xml, misc.xml, codeStyles folder and 126 copyright folder to target folder. Since these files aren't mandatory in 127 IntelliJ, it only logs when an IOError occurred. 128 """ 129 target_path = self.project_info.project_absolute_path 130 idea_dir = os.path.join(target_path, _IDEA_FOLDER) 131 copyright_dir = os.path.join(idea_dir, _COPYRIGHT_FOLDER) 132 code_style_dir = os.path.join(idea_dir, _CODE_STYLE_FOLDER) 133 common_util.file_generate( 134 os.path.join(idea_dir, _COMPILE_XML), templates.XML_COMPILER) 135 common_util.file_generate( 136 os.path.join(idea_dir, _MISC_XML), templates.XML_MISC) 137 common_util.file_generate( 138 os.path.join(copyright_dir, _APACHE_2_XML), templates.XML_APACHE_2) 139 common_util.file_generate( 140 os.path.join(copyright_dir, _PROFILES_SETTINGS_XML), 141 templates.XML_PROFILES_SETTINGS) 142 common_util.file_generate( 143 os.path.join(code_style_dir, _CODE_STYLE_CONFIG_XML), 144 templates.XML_CODE_STYLE_CONFIG) 145 code_style_target_path = os.path.join(code_style_dir, _PROJECT_XML) 146 if os.path.exists(code_style_target_path): 147 os.remove(code_style_target_path) 148 try: 149 shutil.copy2(_CODE_STYLE_SRC_PATH, code_style_target_path) 150 except (OSError, SystemError) as err: 151 logging.warning('%s can\'t copy the project files\n %s', 152 code_style_target_path, err) 153 # Create .gitignore if it doesn't exist. 154 _generate_git_ignore(target_path) 155 # Create jsonSchemas.xml for TEST_MAPPING. 156 _generate_test_mapping_schema(idea_dir) 157 # Create config.json for Asuite plugin 158 lunch_target = common_util.get_lunch_target() 159 if lunch_target: 160 common_util.file_generate( 161 os.path.join(idea_dir, _CONFIG_JSON), lunch_target) 162 163 def _generate_modules_xml(self, iml_path_list=None): 164 """Generate modules.xml file. 165 166 IntelliJ uses modules.xml to import which modules should be loaded to 167 project. In multiple modules case, we will pass iml_path_list of 168 submodules' dependencies and their iml file paths to add them into main 169 module's module.xml file. The dependencies.iml file contains all shared 170 dependencies source folders and jar files. 171 172 Args: 173 iml_path_list: A list of submodule iml paths. 174 """ 175 module_path = self.project_info.project_absolute_path 176 177 # b/121256503: Prevent duplicated iml names from breaking IDEA. 178 module_name = iml.IMLGenerator.get_unique_iml_name(module_path) 179 180 if iml_path_list is not None: 181 module_list = [ 182 _MODULE_SECTION % (module_name, module_name), 183 _MODULE_SECTION % (constant.KEY_DEPENDENCIES, 184 constant.KEY_DEPENDENCIES) 185 ] 186 for iml_path in iml_path_list: 187 module_list.append(_SUB_MODULES_SECTION.format(IML=iml_path)) 188 else: 189 module_list = [ 190 _MODULE_SECTION % (module_name, module_name) 191 ] 192 module = '\n'.join(module_list) 193 content = self._remove_debugger_token(templates.XML_MODULES) 194 content = content.replace(_MODULE_TOKEN, module) 195 target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) 196 common_util.file_generate(target_path, content) 197 198 def _remove_debugger_token(self, content): 199 """Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN. 200 201 Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN in 2 cases: 202 1. Sub projects don't need to be filled in the enable debugger module 203 so we remove the token here. For the main project, the enable 204 debugger module will be appended if it exists at the time launching 205 IDE. 206 2. When there is no need to launch IDE. 207 208 Args: 209 content: The content of module.xml. 210 211 Returns: 212 String: The content of module.xml. 213 """ 214 if (not project_config.ProjectConfig.get_instance().is_launch_ide or 215 not self.project_info.is_main_project): 216 content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, '') 217 return content 218 219 220def _merge_project_vcs_xmls(projects): 221 """Merge sub projects' git paths into main project's vcs.xml. 222 223 After all projects' vcs.xml are generated, collect the git path of each 224 projects and write them into main project's vcs.xml. 225 226 Args: 227 projects: A list of ProjectInfo instances. 228 """ 229 main_project_absolute_path = projects[0].project_absolute_path 230 if main_project_absolute_path != common_util.get_android_root_dir(): 231 git_paths = [common_util.find_git_root(project.project_relative_path) 232 for project in projects if project.project_relative_path] 233 xml_gen.gen_vcs_xml(main_project_absolute_path, git_paths) 234 else: 235 ignore_gits = sorted(_get_all_git_path(main_project_absolute_path)) 236 xml_gen.write_ignore_git_dirs_file(main_project_absolute_path, 237 ignore_gits) 238 239def _get_all_git_path(root_path): 240 """Traverse all subdirectories to get all git folder's path. 241 242 Args: 243 root_path: A string of path to traverse. 244 245 Yields: 246 A git folder's path. 247 """ 248 for dir_path, dir_names, _ in os.walk(root_path): 249 if _GIT_FOLDER_NAME in dir_names: 250 yield dir_path 251 252 253def _generate_git_ignore(target_folder): 254 """Generate .gitignore file. 255 256 In target_folder, if there's no .gitignore file, uses symlink() to generate 257 one to hide project content files from git. 258 259 Args: 260 target_folder: An absolute path string of target folder. 261 """ 262 # TODO(b/133639849): Provide a common method to create symbolic link. 263 # TODO(b/133641803): Move out aidegen artifacts from Android repo. 264 try: 265 gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME) 266 rel_target = os.path.relpath(gitignore_abs_path, os.getcwd()) 267 rel_source = os.path.relpath(_GITIGNORE_ABS_PATH, target_folder) 268 logging.debug('Relative target symlink path: %s.', rel_target) 269 logging.debug('Relative ignore_template source path: %s.', rel_source) 270 if not os.path.exists(gitignore_abs_path): 271 os.symlink(rel_source, rel_target) 272 except OSError as err: 273 logging.error('Not support to run aidegen on Windows.\n %s', err) 274 275 276def _generate_test_mapping_schema(idea_dir): 277 """Create jsonSchemas.xml for TEST_MAPPING. 278 279 Args: 280 idea_dir: An absolute path string of target .idea folder. 281 """ 282 config_path = os.path.join( 283 common_util.get_android_root_dir(), _TEST_MAPPING_CONFIG_PATH) 284 if os.path.isfile(config_path): 285 common_util.file_generate( 286 os.path.join(idea_dir, _JSON_SCHEMAS_CONFIG_XML), 287 templates.TEST_MAPPING_SCHEMAS_XML.format(SCHEMA_PATH=config_path)) 288 else: 289 logging.warning('Can\'t find TEST_MAPPING.config.json') 290 291 292def _filter_out_source_paths(source_paths, module_relpaths): 293 """Filter out the source paths which belong to the target module. 294 295 The source_paths is a union set of all source paths of all target modules. 296 For generating the dependencies.iml, we only need the source paths outside 297 the target modules. 298 299 Args: 300 source_paths: A set contains the source folder paths. 301 module_relpaths: A list, contains the relative paths of target modules 302 except the main module. 303 304 Returns: A set of source paths. 305 """ 306 return {x for x in source_paths if not any( 307 {common_util.is_source_under_relative_path(x, y) 308 for y in module_relpaths})} 309 310 311def update_enable_debugger(module_path, enable_debugger_module_abspath=None): 312 """Append the enable_debugger module's info in modules.xml file. 313 314 Args: 315 module_path: A string of the folder path contains IDE project content, 316 e.g., the folder contains the .idea folder. 317 enable_debugger_module_abspath: A string of the im file path of enable 318 debugger module. 319 """ 320 replace_string = '' 321 if enable_debugger_module_abspath: 322 replace_string = _SUB_MODULES_SECTION.format( 323 IML=enable_debugger_module_abspath) 324 target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) 325 content = common_util.read_file_content(target_path) 326 content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, replace_string) 327 common_util.file_generate(target_path, content) 328 329 330def gen_enable_debugger_module(module_abspath, android_sdk_version): 331 """Generate the enable_debugger module under AIDEGen config folder. 332 333 Skip generating the enable_debugger module in IntelliJ once the attemption 334 of getting the Android SDK version is failed. 335 336 Args: 337 module_abspath: the absolute path of the main project. 338 android_sdk_version: A string, the Android SDK version in jdk.table.xml. 339 """ 340 if not android_sdk_version: 341 return 342 with config.AidegenConfig() as aconf: 343 if aconf.create_enable_debugger_module(android_sdk_version): 344 update_enable_debugger(module_abspath, 345 config.AidegenConfig.DEBUG_ENABLED_FILE_PATH) 346