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