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