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