• 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    projects 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
247def _get_all_git_path(root_path):
248    """Traverse all subdirectories to get all git folder's path.
249
250    Args:
251        root_path: A string of path to traverse.
252
253    Yields:
254        A git folder's path.
255    """
256    for dir_path, dir_names, _ in os.walk(root_path):
257        if _GIT_FOLDER_NAME in dir_names:
258            yield dir_path
259
260
261def _generate_git_ignore(target_folder):
262    """Generate .gitignore file.
263
264    In target_folder, if there's no .gitignore file, generate one to hide
265    project content files from git.
266
267    Args:
268        target_folder: An absolute path string of target folder.
269    """
270    # TODO(b/133641803): Move out aidegen artifacts from Android repo.
271    try:
272        gitignore_abs_path = os.path.join(target_folder, _GITIGNORE_FILE_NAME)
273        if not os.path.exists(gitignore_abs_path):
274            shutil.copy(_GITIGNORE_ABS_PATH, gitignore_abs_path)
275    except OSError as err:
276        logging.error('Not support to run aidegen on Windows.\n %s', err)
277
278
279def _generate_test_mapping_schema(idea_dir):
280    """Create jsonSchemas.xml for TEST_MAPPING.
281
282    Args:
283        idea_dir: An absolute path string of target .idea folder.
284    """
285    config_path = os.path.join(
286        common_util.get_android_root_dir(), _TEST_MAPPING_CONFIG_PATH)
287    if os.path.isfile(config_path):
288        common_util.file_generate(
289            os.path.join(idea_dir, _JSON_SCHEMAS_CONFIG_XML),
290            templates.TEST_MAPPING_SCHEMAS_XML.format(SCHEMA_PATH=config_path))
291    else:
292        logging.warning('Can\'t find TEST_MAPPING.config.json')
293
294
295def _filter_out_source_paths(source_paths, module_relpaths):
296    """Filter out the source paths which belong to the target module.
297
298    The source_paths is a union set of all source paths of all target modules.
299    For generating the dependencies.iml, we only need the source paths outside
300    the target modules.
301
302    Args:
303        source_paths: A set contains the source folder paths.
304        module_relpaths: A list, contains the relative paths of target modules
305                         except the main module.
306
307    Returns: A set of source paths.
308    """
309    return {x for x in source_paths if not any(
310        {common_util.is_source_under_relative_path(x, y)
311         for y in module_relpaths})}
312
313
314def update_enable_debugger(module_path, enable_debugger_module_abspath=None):
315    """Append the enable_debugger module's info in modules.xml file.
316
317    Args:
318        module_path: A string of the folder path contains IDE project content,
319                     e.g., the folder contains the .idea folder.
320        enable_debugger_module_abspath: A string of the im file path of enable
321                                        debugger module.
322    """
323    replace_string = ''
324    if enable_debugger_module_abspath:
325        replace_string = _SUB_MODULES_SECTION.format(
326            IML=enable_debugger_module_abspath)
327    target_path = os.path.join(module_path, _IDEA_FOLDER, _MODULES_XML)
328    content = common_util.read_file_content(target_path)
329    content = content.replace(_ENABLE_DEBUGGER_MODULE_TOKEN, replace_string)
330    common_util.file_generate(target_path, content)
331
332
333def gen_enable_debugger_module(module_abspath, android_sdk_version):
334    """Generate the enable_debugger module under AIDEGen config folder.
335
336    Skip generating the enable_debugger module in IntelliJ once the attemption
337    of getting the Android SDK version is failed.
338
339    Args:
340        module_abspath: the absolute path of the main project.
341        android_sdk_version: A string, the Android SDK version in jdk.table.xml.
342    """
343    if not android_sdk_version:
344        return
345    with config.AidegenConfig() as aconf:
346        if aconf.create_enable_debugger_module(android_sdk_version):
347            update_enable_debugger(module_abspath,
348                                   config.AidegenConfig.DEBUG_ENABLED_FILE_PATH)
349