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