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 project 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 247 248def _get_all_git_path(root_path): 249 """Traverse all subdirectories to get all git folder's path. 250 251 Args: 252 root_path: A string of path to traverse. 253 254 Yields: 255 A git folder's path. 256 """ 257 for dir_path, dir_names, _ in os.walk(root_path): 258 if _GIT_FOLDER_NAME in dir_names: 259 yield dir_path 260 261 262def _generate_git_ignore(target_folder): 263 """Generate .gitignore file. 264 265 In target_folder, if there's no .gitignore file, generate one to hide 266 project content files from git. 267 268 Args: 269 target_folder: An absolute path string of target folder. 270 """ 271 # TODO(b/133641803): Move out aidegen artifacts from Android repo. 272 try: 273 gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME) 274 if not os.path.exists(gitignore_abs_path): 275 shutil.copy(_GITIGNORE_ABS_PATH, gitignore_abs_path) 276 except OSError as err: 277 logging.error('Not support to run aidegen on Windows.\n %s', err) 278 279 280def _generate_test_mapping_schema(idea_dir): 281 """Create jsonSchemas.xml for TEST_MAPPING. 282 283 Args: 284 idea_dir: An absolute path string of target .idea folder. 285 """ 286 config_path = os.path.join( 287 common_util.get_android_root_dir(), _TEST_MAPPING_CONFIG_PATH) 288 if os.path.isfile(config_path): 289 common_util.file_generate( 290 os.path.join(idea_dir, _JSON_SCHEMAS_CONFIG_XML), 291 templates.TEST_MAPPING_SCHEMAS_XML.format(SCHEMA_PATH=config_path)) 292 else: 293 logging.warning('Can\'t find TEST_MAPPING.config.json') 294 295 296def _filter_out_source_paths(source_paths, module_relpaths): 297 """Filter out the source paths which belong to the target module. 298 299 The source_paths is a union set of all source paths of all target modules. 300 For generating the dependencies.iml, we only need the source paths outside 301 the target modules. 302 303 Args: 304 source_paths: A set contains the source folder paths. 305 module_relpaths: A list, contains the relative paths of target modules 306 except the main module. 307 308 Returns: A set of source paths. 309 """ 310 return {x for x in source_paths if not any( 311 {common_util.is_source_under_relative_path(x, y) 312 for y in module_relpaths})} 313 314 315def update_enable_debugger(module_path, enable_debugger_module_abspath=None): 316 """Append the enable_debugger module's info in modules.xml file. 317 318 Args: 319 module_path: A string of the folder path contains IDE project content, 320 e.g., the folder contains the .idea folder. 321 enable_debugger_module_abspath: A string of the im file path of enable 322 debugger module. 323 """ 324 replace_string = '' 325 if enable_debugger_module_abspath: 326 replace_string = _SUB_MODULES_SECTION.format( 327 IML=enable_debugger_module_abspath) 328 target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) 329 content = common_util.read_file_content(target_path) 330 content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, replace_string) 331 common_util.file_generate(target_path, content) 332 333 334def gen_enable_debugger_module(module_abspath, android_sdk_version): 335 """Generate the enable_debugger module under AIDEGen config folder. 336 337 Skip generating the enable_debugger module in IntelliJ once the attempt 338 of getting the Android SDK version is failed. 339 340 Args: 341 module_abspath: the absolute path of the main project. 342 android_sdk_version: A string, the Android SDK version in jdk.table.xml. 343 """ 344 if not android_sdk_version: 345 return 346 with config.AidegenConfig() as aconf: 347 if aconf.create_enable_debugger_module(android_sdk_version): 348 update_enable_debugger(module_abspath, 349 config.AidegenConfig.DEBUG_ENABLED_FILE_PATH) 350