| #!/usr/bin/env python3 |
| # |
| # Copyright 2018 - The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """It is an AIDEGen sub task : generate the project files. |
| |
| Usage example: |
| projects: A list of ProjectInfo instances. |
| ProjectFileGenerator.generate_ide_project_file(projects) |
| """ |
| |
| import logging |
| import os |
| import shutil |
| |
| from aidegen import constant |
| from aidegen import templates |
| from aidegen.idea import iml |
| from aidegen.idea import xml_gen |
| from aidegen.lib import common_util |
| from aidegen.lib import config |
| from aidegen.lib import project_config |
| from aidegen.project import project_splitter |
| |
| # FACET_SECTION is a part of iml, which defines the framework of the project. |
| _MODULE_SECTION = (' <module fileurl="file:///$PROJECT_DIR$/%s.iml"' |
| ' filepath="$PROJECT_DIR$/%s.iml" />') |
| _SUB_MODULES_SECTION = (' <module fileurl="file:///{IML}" ' |
| 'filepath="{IML}" />') |
| _MODULE_TOKEN = '@MODULES@' |
| _ENABLE_DEBUGGER_MODULE_TOKEN = '@ENABLE_DEBUGGER_MODULE@' |
| _IDEA_FOLDER = '.idea' |
| _MODULES_XML = 'modules.xml' |
| _COPYRIGHT_FOLDER = 'copyright' |
| _INSPECTION_FOLDER = 'inspectionProfiles' |
| _CODE_STYLE_FOLDER = 'codeStyles' |
| _APACHE_2_XML = 'Apache_2.xml' |
| _PROFILES_SETTINGS_XML = 'profiles_settings.xml' |
| _CODE_STYLE_CONFIG_XML = 'codeStyleConfig.xml' |
| _INSPECTION_CONFIG_XML = 'Aidegen_Inspections.xml' |
| _JSON_SCHEMAS_CONFIG_XML = 'jsonSchemas.xml' |
| _PROJECT_XML = 'Project.xml' |
| _COMPILE_XML = 'compiler.xml' |
| _MISC_XML = 'misc.xml' |
| _CONFIG_JSON = 'config.json' |
| _GIT_FOLDER_NAME = '.git' |
| # Support gitignore by symbolic link to aidegen/data/gitignore_template. |
| _GITIGNORE_FILE_NAME = '.gitignore' |
| _GITIGNORE_REL_PATH = 'tools/asuite/aidegen/data/gitignore_template' |
| _GITIGNORE_ABS_PATH = os.path.join(common_util.get_android_root_dir(), |
| _GITIGNORE_REL_PATH) |
| # Support code style by symbolic link to aidegen/data/AndroidStyle_aidegen.xml. |
| _CODE_STYLE_REL_PATH = 'tools/asuite/aidegen/data/AndroidStyle_aidegen.xml' |
| _CODE_STYLE_SRC_PATH = os.path.join(common_util.get_android_root_dir(), |
| _CODE_STYLE_REL_PATH) |
| _TEST_MAPPING_CONFIG_PATH = ('tools/tradefederation/core/src/com/android/' |
| 'tradefed/util/testmapping/TEST_MAPPING.config' |
| '.json') |
| |
| |
| class ProjectFileGenerator: |
| """Project file generator. |
| |
| Attributes: |
| project_info: A instance of ProjectInfo. |
| """ |
| |
| def __init__(self, project_info): |
| """ProjectFileGenerator initialize. |
| |
| Args: |
| project_info: A instance of ProjectInfo. |
| """ |
| self.project_info = project_info |
| |
| def generate_intellij_project_file(self, iml_path_list=None): |
| """Generates IntelliJ project file. |
| |
| # TODO(b/155346505): Move this method to idea folder. |
| |
| Args: |
| iml_path_list: An optional list of submodule's iml paths, the |
| default value is None. |
| """ |
| if self.project_info.is_main_project: |
| self._generate_modules_xml(iml_path_list) |
| self._copy_constant_project_files() |
| |
| @classmethod |
| def generate_ide_project_files(cls, projects): |
| """Generate IDE project files by a list of ProjectInfo instances. |
| |
| It deals with the sources by ProjectSplitter to create iml files for |
| each project and generate_intellij_project_file only creates |
| the other project files under .idea/. |
| |
| Args: |
| projects: A list of ProjectInfo instances. |
| """ |
| # Initialization |
| iml.IMLGenerator.USED_NAME_CACHE.clear() |
| proj_splitter = project_splitter.ProjectSplitter(projects) |
| proj_splitter.get_dependencies() |
| proj_splitter.revise_source_folders() |
| iml_paths = [proj_splitter.gen_framework_srcjars_iml()] |
| proj_splitter.gen_projects_iml() |
| iml_paths += [project.iml_path for project in projects] |
| ProjectFileGenerator( |
| projects[0]).generate_intellij_project_file(iml_paths) |
| _merge_project_vcs_xmls(projects) |
| |
| def _copy_constant_project_files(self): |
| """Copy project files to target path with error handling. |
| |
| This function would copy compiler.xml, misc.xml, codeStyles folder and |
| copyright folder to target folder. Since these files aren't mandatory in |
| IntelliJ, it only logs when an IOError occurred. |
| """ |
| target_path = self.project_info.project_absolute_path |
| idea_dir = os.path.join(target_path, _IDEA_FOLDER) |
| copyright_dir = os.path.join(idea_dir, _COPYRIGHT_FOLDER) |
| inspection_dir = os.path.join(idea_dir, _INSPECTION_FOLDER) |
| code_style_dir = os.path.join(idea_dir, _CODE_STYLE_FOLDER) |
| common_util.file_generate( |
| os.path.join(idea_dir, _COMPILE_XML), templates.XML_COMPILER) |
| common_util.file_generate( |
| os.path.join(idea_dir, _MISC_XML), templates.XML_MISC) |
| common_util.file_generate( |
| os.path.join(copyright_dir, _APACHE_2_XML), templates.XML_APACHE_2) |
| common_util.file_generate( |
| os.path.join(copyright_dir, _PROFILES_SETTINGS_XML), |
| templates.XML_COPYRIGHT_PROFILES_SETTINGS) |
| common_util.file_generate( |
| os.path.join(inspection_dir, _PROFILES_SETTINGS_XML), |
| templates.XML_INSPECTION_PROFILES_SETTINGS) |
| inspection_config_xml = os.path.join(inspection_dir, |
| _INSPECTION_CONFIG_XML) |
| if not os.path.exists(inspection_config_xml): |
| common_util.file_generate(inspection_config_xml, |
| templates.XML_INSPECTIONS) |
| common_util.file_generate( |
| os.path.join(code_style_dir, _CODE_STYLE_CONFIG_XML), |
| templates.XML_CODE_STYLE_CONFIG) |
| code_style_target_path = os.path.join(code_style_dir, _PROJECT_XML) |
| if not os.path.exists(code_style_target_path): |
| try: |
| shutil.copy2(_CODE_STYLE_SRC_PATH, code_style_target_path) |
| except (OSError, SystemError) as err: |
| logging.warning('%s can\'t copy the project files\n %s', |
| code_style_target_path, err) |
| # Create .gitignore if it doesn't exist. |
| _generate_git_ignore(target_path) |
| # Create jsonSchemas.xml for TEST_MAPPING. |
| _generate_test_mapping_schema(idea_dir) |
| # Create config.json for Asuite plugin |
| lunch_target = common_util.get_lunch_target() |
| if lunch_target: |
| common_util.file_generate( |
| os.path.join(idea_dir, _CONFIG_JSON), lunch_target) |
| |
| def _generate_modules_xml(self, iml_path_list=None): |
| """Generate modules.xml file. |
| |
| IntelliJ uses modules.xml to import which modules should be loaded to |
| project. In multiple modules case, we will pass iml_path_list of |
| submodules' dependencies and their iml file paths to add them into main |
| module's module.xml file. The dependencies.iml file contains all shared |
| dependencies source folders and jar files. |
| |
| Args: |
| iml_path_list: A list of submodule iml paths. |
| """ |
| module_path = self.project_info.project_absolute_path |
| |
| # b/121256503: Prevent duplicated iml names from breaking IDEA. |
| module_name = iml.IMLGenerator.get_unique_iml_name(module_path) |
| |
| if iml_path_list is not None: |
| module_list = [ |
| _MODULE_SECTION % (module_name, module_name), |
| _MODULE_SECTION % (constant.KEY_DEPENDENCIES, |
| constant.KEY_DEPENDENCIES) |
| ] |
| for iml_path in iml_path_list: |
| module_list.append(_SUB_MODULES_SECTION.format(IML=iml_path)) |
| else: |
| module_list = [ |
| _MODULE_SECTION % (module_name, module_name) |
| ] |
| module = '\n'.join(module_list) |
| content = self._remove_debugger_token(templates.XML_MODULES) |
| content = content.replace(_MODULE_TOKEN, module) |
| target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) |
| common_util.file_generate(target_path, content) |
| |
| def _remove_debugger_token(self, content): |
| """Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN. |
| |
| Remove the token _ENABLE_DEBUGGER_MODULE_TOKEN in 2 cases: |
| 1. Sub-projects don't need to be filled in the enable debugger module |
| so we remove the token here. For the main project, the enable |
| debugger module will be appended if it exists at the time launching |
| IDE. |
| 2. When there is no need to launch IDE. |
| |
| Args: |
| content: The content of module.xml. |
| |
| Returns: |
| String: The content of module.xml. |
| """ |
| if (not project_config.ProjectConfig.get_instance().is_launch_ide or |
| not self.project_info.is_main_project): |
| content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, '') |
| return content |
| |
| |
| def _merge_project_vcs_xmls(projects): |
| """Merge sub-projects' git paths into main project's vcs.xml. |
| |
| After all projects' vcs.xml are generated, collect the git path of each |
| project and write them into main project's vcs.xml. |
| |
| Args: |
| projects: A list of ProjectInfo instances. |
| """ |
| main_project_absolute_path = projects[0].project_absolute_path |
| if main_project_absolute_path != common_util.get_android_root_dir(): |
| git_paths = [common_util.find_git_root(project.project_relative_path) |
| for project in projects if project.project_relative_path] |
| xml_gen.gen_vcs_xml(main_project_absolute_path, git_paths) |
| else: |
| ignore_gits = sorted(_get_all_git_path(main_project_absolute_path)) |
| xml_gen.write_ignore_git_dirs_file(main_project_absolute_path, |
| ignore_gits) |
| |
| |
| def _get_all_git_path(root_path): |
| """Traverse all subdirectories to get all git folder's path. |
| |
| Args: |
| root_path: A string of path to traverse. |
| |
| Yields: |
| A git folder's path. |
| """ |
| for dir_path, dir_names, _ in os.walk(root_path): |
| if _GIT_FOLDER_NAME in dir_names: |
| yield dir_path |
| |
| |
| def _generate_git_ignore(target_folder): |
| """Generate .gitignore file. |
| |
| In target_folder, if there's no .gitignore file, generate one to hide |
| project content files from git. |
| |
| Args: |
| target_folder: An absolute path string of target folder. |
| """ |
| # TODO(b/133641803): Move out aidegen artifacts from Android repo. |
| try: |
| gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME) |
| if not os.path.exists(gitignore_abs_path): |
| shutil.copy(_GITIGNORE_ABS_PATH, gitignore_abs_path) |
| except OSError as err: |
| logging.error('Not support to run aidegen on Windows.\n %s', err) |
| |
| |
| def _generate_test_mapping_schema(idea_dir): |
| """Create jsonSchemas.xml for TEST_MAPPING. |
| |
| Args: |
| idea_dir: An absolute path string of target .idea folder. |
| """ |
| config_path = os.path.join( |
| common_util.get_android_root_dir(), _TEST_MAPPING_CONFIG_PATH) |
| if os.path.isfile(config_path): |
| common_util.file_generate( |
| os.path.join(idea_dir, _JSON_SCHEMAS_CONFIG_XML), |
| templates.TEST_MAPPING_SCHEMAS_XML.format(SCHEMA_PATH=config_path)) |
| else: |
| logging.warning('Can\'t find TEST_MAPPING.config.json') |
| |
| |
| def _filter_out_source_paths(source_paths, module_relpaths): |
| """Filter out the source paths which belong to the target module. |
| |
| The source_paths is a union set of all source paths of all target modules. |
| For generating the dependencies.iml, we only need the source paths outside |
| the target modules. |
| |
| Args: |
| source_paths: A set contains the source folder paths. |
| module_relpaths: A list, contains the relative paths of target modules |
| except the main module. |
| |
| Returns: A set of source paths. |
| """ |
| return {x for x in source_paths if not any( |
| {common_util.is_source_under_relative_path(x, y) |
| for y in module_relpaths})} |
| |
| |
| def update_enable_debugger(module_path, enable_debugger_module_abspath=None): |
| """Append the enable_debugger module's info in modules.xml file. |
| |
| Args: |
| module_path: A string of the folder path contains IDE project content, |
| e.g., the folder contains the .idea folder. |
| enable_debugger_module_abspath: A string of the im file path of enable |
| debugger module. |
| """ |
| replace_string = '' |
| if enable_debugger_module_abspath: |
| replace_string = _SUB_MODULES_SECTION.format( |
| IML=enable_debugger_module_abspath) |
| target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML) |
| content = common_util.read_file_content(target_path) |
| content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, replace_string) |
| common_util.file_generate(target_path, content) |
| |
| |
| def gen_enable_debugger_module(module_abspath, android_sdk_version): |
| """Generate the enable_debugger module under AIDEGen config folder. |
| |
| Skip generating the enable_debugger module in IntelliJ once the attempt |
| of getting the Android SDK version is failed. |
| |
| Args: |
| module_abspath: the absolute path of the main project. |
| android_sdk_version: A string, the Android SDK version in jdk.table.xml. |
| """ |
| if not android_sdk_version: |
| return |
| with config.AidegenConfig() as aconf: |
| if aconf.create_enable_debugger_module(android_sdk_version): |
| update_enable_debugger(module_abspath, |
| config.AidegenConfig.DEBUG_ENABLED_FILE_PATH) |