1#!/usr/bin/env python3
2#
3# Copyright 2020 - 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"""Separate the sources from multiple projects."""
18
19import os
20
21from aidegen import constant
22from aidegen.idea import iml
23from aidegen.lib import common_util
24from aidegen.lib import project_config
25
26_KEY_SOURCE_PATH = 'source_folder_path'
27_KEY_TEST_PATH = 'test_folder_path'
28_SOURCE_FOLDERS = [_KEY_SOURCE_PATH, _KEY_TEST_PATH]
29_KEY_SRCJAR_PATH = 'srcjar_path'
30_KEY_R_PATH = 'r_java_path'
31_KEY_JAR_PATH = 'jar_path'
32_EXCLUDE_ITEM = '\n            <excludeFolder url="file://%s" />'
33# Temporarily exclude test-dump and src_stub folders to prevent symbols from
34# resolving failure by incorrect reference. These two folders should be removed
35# after b/136982078 is resolved.
36_EXCLUDE_FOLDERS = ['.idea', '.repo', 'art', 'bionic', 'bootable', 'build',
37                    'dalvik', 'developers', 'device', 'hardware', 'kernel',
38                    'libnativehelper', 'pdk', 'prebuilts', 'sdk', 'system',
39                    'toolchain', 'tools', 'vendor', 'out',
40                    'art/tools/ahat/src/test-dump',
41                    'cts/common/device-side/device-info/src_stub']
42
43
44class ProjectSplitter:
45    """Splits the sources from multiple projects.
46
47    It's a specific solution to deal with the source folders in multiple
48    project case. Since the IntelliJ does not allow duplicate source folders,
49    AIDEGen needs to separate the source folders for each project. The single
50    project case has no different with current structure.
51
52    Usage:
53    project_splitter = ProjectSplitter(projects)
54
55    # Find the dependencies between the projects.
56    project_splitter.get_dependencies()
57
58    # Clear the source folders for each project.
59    project_splitter.revise_source_folders()
60
61    Attributes:
62        _projects: A list of ProjectInfo.
63        _all_srcs: A dictionary contains all sources of multiple projects.
64                   e.g.
65                   {
66                       'module_name': 'test',
67                       'path': ['path/to/module'],
68                       'srcs': ['src_folder1', 'src_folder2'],
69                       'tests': ['test_folder1', 'test_folder2']
70                       'jars': ['jar1.jar'],
71                       'srcjars': ['1.srcjar', '2.srcjar'],
72                       'dependencies': ['framework_srcjars', 'base'],
73                       'iml_name': '/abs/path/to/iml.iml'
74                   }
75        _framework_exist: A boolean, True if framework is one of the projects.
76        _framework_iml: A string, the name of the framework's iml.
77        _full_repo: A boolean, True if loading with full Android sources.
78        _full_repo_iml: A string, the name of the Android folder's iml.
79    """
80    def __init__(self, projects):
81        """ProjectSplitter initialize.
82
83        Args:
84            projects: A list of ProjectInfo object.
85        """
86        self._projects = projects
87        self._all_srcs = dict(projects[0].source_path)
88        self._framework_iml = None
89        self._framework_exist = any(
90            {p.project_relative_path == constant.FRAMEWORK_PATH
91             for p in self._projects})
92        if self._framework_exist:
93            self._framework_iml = iml.IMLGenerator.get_unique_iml_name(
94                os.path.join(common_util.get_android_root_dir(),
95                             constant.FRAMEWORK_PATH))
96        self._full_repo = project_config.ProjectConfig.get_instance().full_repo
97        if self._full_repo:
98            self._full_repo_iml = os.path.basename(
99                common_util.get_android_root_dir())
100
101    def revise_source_folders(self):
102        """Resets the source folders of each project.
103
104        There should be no duplicate source root path in IntelliJ. The issue
105        doesn't happen in single project case. Once users choose multiple
106        projects, there could be several same source paths of different
107        projects. In order to prevent that, we should remove the source paths
108        in dependencies.iml which are duplicate with the paths in [module].iml
109        files.
110
111        Steps to prevent the duplicate source root path in IntelliJ:
112        1. Copy all sources from sub-projects to main project.
113        2. Delete the source and test folders which are not under the
114           sub-projects.
115        3. Delete the sub-projects' source and test paths from the main project.
116        """
117        self._collect_all_srcs()
118        self._keep_local_sources()
119        self._remove_duplicate_sources()
120
121    def _collect_all_srcs(self):
122        """Copies all projects' sources to a dictionary."""
123        for project in self._projects[1:]:
124            for key, value in project.source_path.items():
125                self._all_srcs[key].update(value)
126
127    def _keep_local_sources(self):
128        """Removes source folders which are not under the project's path.
129
130        1. Remove the source folders which are not under the project.
131        2. Remove the duplicate project's source folders from the _all_srcs.
132        """
133        for project in self._projects:
134            srcs = project.source_path
135            relpath = project.project_relative_path
136            is_root = not relpath
137            for key in _SOURCE_FOLDERS:
138                srcs[key] = {s for s in srcs[key]
139                             if common_util.is_source_under_relative_path(
140                                 s, relpath) or is_root}
141                self._all_srcs[key] -= srcs[key]
142
143    def _remove_duplicate_sources(self):
144        """Removes the duplicate source folders from each sub project.
145
146        Priority processing with the longest path length, e.g.
147        frameworks/base/packages/SettingsLib must have priority over
148        frameworks/base.
149        """
150        for child in sorted(self._projects, key=lambda k: len(
151                k.project_relative_path), reverse=True):
152            for parent in self._projects:
153                is_root = not parent.project_relative_path
154                if parent is child:
155                    continue
156                if (common_util.is_source_under_relative_path(
157                        child.project_relative_path,
158                        parent.project_relative_path) or is_root):
159                    for key in _SOURCE_FOLDERS:
160                        parent.source_path[key] -= child.source_path[key]
161
162    def get_dependencies(self):
163        """Gets the dependencies between the projects.
164
165        Check if the current project's source folder exists in other projects.
166        If do, the current project is a dependency module to the other.
167        """
168        for project in sorted(self._projects, key=lambda k: len(
169                k.project_relative_path)):
170            proj_path = project.project_relative_path
171            project.dependencies = [constant.FRAMEWORK_SRCJARS]
172            if self._framework_exist and proj_path != constant.FRAMEWORK_PATH:
173                project.dependencies.append(self._framework_iml)
174            if self._full_repo and proj_path:
175                project.dependencies.append(self._full_repo_iml)
176            srcs = (project.source_path[_KEY_SOURCE_PATH]
177                    | project.source_path[_KEY_TEST_PATH])
178            for dep_proj in sorted(self._projects, key=lambda k: len(
179                    k.project_relative_path)):
180                dep_path = dep_proj.project_relative_path
181                is_root = not dep_path
182                is_child = common_util.is_source_under_relative_path(dep_path,
183                                                                     proj_path)
184                is_dep = any({s for s in srcs
185                              if common_util.is_source_under_relative_path(
186                                  s, dep_path) or is_root})
187                if dep_proj is project or is_child or not is_dep:
188                    continue
189                dep = iml.IMLGenerator.get_unique_iml_name(os.path.join(
190                    common_util.get_android_root_dir(), dep_path))
191                if dep not in project.dependencies:
192                    project.dependencies.append(dep)
193            project.dependencies.append(constant.KEY_DEPENDENCIES)
194
195    def gen_framework_srcjars_iml(self):
196        """Generates the framework-srcjars.iml.
197
198        Create the iml file with only the srcjars of module framework-all. These
199        srcjars will be separated from the modules under frameworks/base.
200
201        Returns:
202            A string of the framework_srcjars.iml's absolute path.
203        """
204        mod = dict(self._projects[0].dep_modules[constant.FRAMEWORK_ALL])
205        mod[constant.KEY_DEPENDENCIES] = []
206        mod[constant.KEY_IML_NAME] = constant.FRAMEWORK_SRCJARS
207        if self._framework_exist:
208            mod[constant.KEY_DEPENDENCIES].append(self._framework_iml)
209        if self._full_repo:
210            mod[constant.KEY_DEPENDENCIES].append(self._full_repo_iml)
211        mod[constant.KEY_DEPENDENCIES].append(constant.KEY_DEPENDENCIES)
212        framework_srcjars_iml = iml.IMLGenerator(mod)
213        framework_srcjars_iml.create({constant.KEY_SRCJARS: True,
214                                      constant.KEY_DEPENDENCIES: True})
215        self._all_srcs[_KEY_SRCJAR_PATH] -= set(mod[constant.KEY_SRCJARS])
216        return framework_srcjars_iml.iml_path
217
218    def _gen_dependencies_iml(self):
219        """Generates the dependencies.iml."""
220        mod = {
221            constant.KEY_SRCS: self._all_srcs[_KEY_SOURCE_PATH],
222            constant.KEY_TESTS: self._all_srcs[_KEY_TEST_PATH],
223            constant.KEY_JARS: self._all_srcs[_KEY_JAR_PATH],
224            constant.KEY_SRCJARS: (self._all_srcs[_KEY_R_PATH]
225                                   | self._all_srcs[_KEY_SRCJAR_PATH]),
226            constant.KEY_DEPENDENCIES: [constant.FRAMEWORK_SRCJARS],
227            constant.KEY_PATH: [self._projects[0].project_relative_path],
228            constant.KEY_MODULE_NAME: constant.KEY_DEPENDENCIES,
229            constant.KEY_IML_NAME: constant.KEY_DEPENDENCIES
230        }
231        if self._framework_exist:
232            mod[constant.KEY_DEPENDENCIES].append(self._framework_iml)
233        if self._full_repo:
234            mod[constant.KEY_DEPENDENCIES].append(self._full_repo_iml)
235        dep_iml = iml.IMLGenerator(mod)
236        dep_iml.create({constant.KEY_DEP_SRCS: True,
237                        constant.KEY_SRCJARS: True,
238                        constant.KEY_JARS: True,
239                        constant.KEY_DEPENDENCIES: True})
240
241    def gen_projects_iml(self):
242        """Generates the projects' iml file."""
243        root_path = common_util.get_android_root_dir()
244        excludes = project_config.ProjectConfig.get_instance().exclude_paths
245        for project in self._projects:
246            relpath = project.project_relative_path
247            exclude_folders = []
248            if not relpath:
249                exclude_folders.extend(get_exclude_content(root_path))
250            if excludes:
251                exclude_folders.extend(get_exclude_content(root_path, excludes))
252            mod_info = {
253                constant.KEY_EXCLUDES: ''.join(exclude_folders),
254                constant.KEY_SRCS: project.source_path[_KEY_SOURCE_PATH],
255                constant.KEY_TESTS: project.source_path[_KEY_TEST_PATH],
256                constant.KEY_DEPENDENCIES: project.dependencies,
257                constant.KEY_PATH: [relpath],
258                constant.KEY_MODULE_NAME: project.module_name,
259                constant.KEY_IML_NAME: iml.IMLGenerator.get_unique_iml_name(
260                    os.path.join(root_path, relpath))
261            }
262            dep_iml = iml.IMLGenerator(mod_info)
263            dep_iml.create({constant.KEY_SRCS: True,
264                            constant.KEY_DEPENDENCIES: True})
265            project.iml_path = dep_iml.iml_path
266        self._gen_dependencies_iml()
267
268
269def get_exclude_content(root_path, excludes=None):
270    """Get the exclude folder content list.
271
272    It returns the exclude folders content list.
273    e.g.
274    ['<excludeFolder url="file://a/.idea" />',
275    '<excludeFolder url="file://a/.repo" />']
276
277    Args:
278        root_path: Android source file path.
279        excludes: A list of exclusive directories, the default value is None but
280                  will be assigned to _EXCLUDE_FOLDERS.
281
282    Returns:
283        String: exclude folder content list.
284    """
285    exclude_items = []
286    if not excludes:
287        excludes = _EXCLUDE_FOLDERS
288    for folder in excludes:
289        folder_path = os.path.join(root_path, folder)
290        if os.path.isdir(folder_path):
291            exclude_items.append(_EXCLUDE_ITEM % folder_path)
292    return exclude_items
293