• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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