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 common_util.file_generate( 149 os.path.join(inspection_dir, _INSPECTION_CONFIG_XML), 150 templates.XML_INSPECTIONS) 151 common_util.file_generate( 152 os.path.join(code_style_dir, _CODE_STYLE_CONFIG_XML), 153 templates.XML_CODE_STYLE_CONFIG) 154 code_style_target_path = os.path.join(code_style_dir, _PROJECT_XML) 155 if not os.path.exists(code_style_target_path): 156 try: 157 shutil.copy2(_CODE_STYLE_SRC_PATH, code_style_target_path) 158 except (OSError, SystemError) as err: 159 logging.warning('%s can\'t copy the project files\n %s', 160 code_style_target_path, err) 161 # Create .gitignore if it doesn't exist. 162 _generate_git_ignore(target_path) 163 # Create jsonSchemas.xml for TEST_MAPPING. 164 _generate_test_mapping_schema(idea_dir) 165 # Create config.json for Asuite plugin 166 lunch_target = common_util.get_lunch_target() 167 if lunch_target: 168 common_util.file_generate( 169 os.path.join(idea_dir, _CONFIG_JSON), lunch_target) 170 171 def _generate_modules_xml(self, iml_path_list=None): 172 """Generate modules.xml file. 173 174 IntelliJ uses modules.xml to import which modules should be loaded to 175 project. In multiple modules case, we will pass iml_path_list of 176 submodules' dependencies and their iml file paths to add them into main 177 module's module.xml file. The dependencies.iml file contains all shared 178 dependencies source folders and jar files. 179 180 Args: 181 iml_path_list: A list of submodule iml paths. 182 """ 183 module_path = self.project_info.project_absolute_path 184 185 # b/121256503: Prevent duplicated iml names from breaking IDEA. 186 module_name = iml.IMLGenerator.get_unique_iml_name(module_path) 187 188 if iml_path_list is not None: 189 module_list = [ 190 _MODULE_SECTION % (module_name, module_name), 191 _MODULE_SECTION % (constant.KEY_DEPENDENCIES, 192 constant.KEY_DEPENDENCIES) 193 ] 194 for iml_path in iml_path_list: 195 module_list.append(_SUB_MODULES_SECTION.format(IML=iml_path)) 196 else: 197 module_list = [ 198 _MODULE_SECTION % (module_name, module_name) 199 ] 200 module = '\n'.join(module_list) 201 content = self._remove_debugger_token(templates.XML_MODULES) 202 content = content.replace(_MODULE_TOKEN, module) 203 target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) 204 common_util.file_generate(target_path, content) 205 206 def _remove_debugger_token(self, content): 207 """Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN. 208 209 Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN in 2 cases: 210 1. Sub projects don't need to be filled in the enable debugger module 211 so we remove the token here. For the main project, the enable 212 debugger module will be appended if it exists at the time launching 213 IDE. 214 2. When there is no need to launch IDE. 215 216 Args: 217 content: The content of module.xml. 218 219 Returns: 220 String: The content of module.xml. 221 """ 222 if (not project_config.ProjectConfig.get_instance().is_launch_ide or 223 not self.project_info.is_main_project): 224 content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, '') 225 return content 226 227 228def _merge_project_vcs_xmls(projects): 229 """Merge sub projects' git paths into main project's vcs.xml. 230 231 After all projects' vcs.xml are generated, collect the git path of each 232 projects and write them into main project's vcs.xml. 233 234 Args: 235 projects: A list of ProjectInfo instances. 236 """ 237 main_project_absolute_path = projects[0].project_absolute_path 238 if main_project_absolute_path != common_util.get_android_root_dir(): 239 git_paths = [common_util.find_git_root(project.project_relative_path) 240 for project in projects if project.project_relative_path] 241 xml_gen.gen_vcs_xml(main_project_absolute_path, git_paths) 242 else: 243 ignore_gits = sorted(_get_all_git_path(main_project_absolute_path)) 244 xml_gen.write_ignore_git_dirs_file(main_project_absolute_path, 245 ignore_gits) 246 247def _get_all_git_path(root_path): 248 """Traverse all subdirectories to get all git folder's path. 249 250 Args: 251 root_path: A string of path to traverse. 252 253 Yields: 254 A git folder's path. 255 """ 256 for dir_path, dir_names, _ in os.walk(root_path): 257 if _GIT_FOLDER_NAME in dir_names: 258 yield dir_path 259 260 261def _generate_git_ignore(target_folder): 262 """Generate .gitignore file. 263 264 In target_folder, if there's no .gitignore file, generate one to hide 265 project content files from git. 266 267 Args: 268 target_folder: An absolute path string of target folder. 269 """ 270 # TODO(b/133641803): Move out aidegen artifacts from Android repo. 271 try: 272 gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME) 273 if not os.path.exists(gitignore_abs_path): 274 shutil.copy(_GITIGNORE_ABS_PATH, gitignore_abs_path) 275 except OSError as err: 276 logging.error('Not support to run aidegen on Windows.\n %s', err) 277 278 279def _generate_test_mapping_schema(idea_dir): 280 """Create jsonSchemas.xml for TEST_MAPPING. 281 282 Args: 283 idea_dir: An absolute path string of target .idea folder. 284 """ 285 config_path = os.path.join( 286 common_util.get_android_root_dir(), _TEST_MAPPING_CONFIG_PATH) 287 if os.path.isfile(config_path): 288 common_util.file_generate( 289 os.path.join(idea_dir, _JSON_SCHEMAS_CONFIG_XML), 290 templates.TEST_MAPPING_SCHEMAS_XML.format(SCHEMA_PATH=config_path)) 291 else: 292 logging.warning('Can\'t find TEST_MAPPING.config.json') 293 294 295def _filter_out_source_paths(source_paths, module_relpaths): 296 """Filter out the source paths which belong to the target module. 297 298 The source_paths is a union set of all source paths of all target modules. 299 For generating the dependencies.iml, we only need the source paths outside 300 the target modules. 301 302 Args: 303 source_paths: A set contains the source folder paths. 304 module_relpaths: A list, contains the relative paths of target modules 305 except the main module. 306 307 Returns: A set of source paths. 308 """ 309 return {x for x in source_paths if not any( 310 {common_util.is_source_under_relative_path(x, y) 311 for y in module_relpaths})} 312 313 314def update_enable_debugger(module_path, enable_debugger_module_abspath=None): 315 """Append the enable_debugger module's info in modules.xml file. 316 317 Args: 318 module_path: A string of the folder path contains IDE project content, 319 e.g., the folder contains the .idea folder. 320 enable_debugger_module_abspath: A string of the im file path of enable 321 debugger module. 322 """ 323 replace_string = '' 324 if enable_debugger_module_abspath: 325 replace_string = _SUB_MODULES_SECTION.format( 326 IML=enable_debugger_module_abspath) 327 target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) 328 content = common_util.read_file_content(target_path) 329 content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, replace_string) 330 common_util.file_generate(target_path, content) 331 332 333def gen_enable_debugger_module(module_abspath, android_sdk_version): 334 """Generate the enable_debugger module under AIDEGen config folder. 335 336 Skip generating the enable_debugger module in IntelliJ once the attemption 337 of getting the Android SDK version is failed. 338 339 Args: 340 module_abspath: the absolute path of the main project. 341 android_sdk_version: A string, the Android SDK version in jdk.table.xml. 342 """ 343 if not android_sdk_version: 344 return 345 with config.AidegenConfig() as aconf: 346 if aconf.create_enable_debugger_module(android_sdk_version): 347 update_enable_debugger(module_abspath, 348 config.AidegenConfig.DEBUG_ENABLED_FILE_PATH) 349