• 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_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