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"""Creates the iml file for each module.
18
19This class is used to create the iml file for each module. So far, only generate
20the create_srcjar() for the framework-all module.
21
22Usage example:
23    modules_info = project_info.ProjectInfo.modules_info
24    mod_info = modules_info.name_to_module_info['module']
25    iml = IMLGenerator(mod_info)
26    iml.create()
27"""
28
29from __future__ import absolute_import
30
31import logging
32import os
33
34from aidegen import constant
35from aidegen import templates
36from aidegen.lib import common_util
37
38
39class IMLGenerator:
40    """Creates the iml file for each module.
41
42    Class attributes:
43        _USED_NAME_CACHE: A dict to cache already used iml project file names
44                          and prevent duplicated iml names from breaking IDEA.
45
46    Attributes:
47        _mod_info: A dictionary of the module's data from module-info.json.
48        _android_root: A string ot the Android root's absolute path.
49        _mod_path: A string of the module's absolute path.
50        _iml_path: A string of the module's iml absolute path.
51        _facet: A string of the facet setting.
52        _excludes: A string of the exclude relative paths.
53        _srcs: A string of the source urls.
54        _jars: A list of the jar urls.
55        _srcjars: A list of srcjar urls.
56        _deps: A list of the dependency module urls.
57    """
58    # b/121256503: Prevent duplicated iml names from breaking IDEA.
59    # Use a map to cache in-using(already used) iml project file names.
60    USED_NAME_CACHE = dict()
61
62    def __init__(self, mod_info):
63        """Initializes IMLGenerator.
64
65        Args:
66            mod_info: A dictionary of the module's data from module-info.json.
67        """
68        self._mod_info = mod_info
69        self._android_root = common_util.get_android_root_dir()
70        self._mod_path = os.path.join(self._android_root,
71                                      mod_info[constant.KEY_PATH][0])
72        self._iml_path = os.path.join(self._mod_path,
73                                      mod_info[constant.KEY_IML_NAME] + '.iml')
74        self._facet = ''
75        self._excludes = ''
76        self._srcs = ''
77        self._jars = []
78        self._srcjars = []
79        self._deps = []
80
81    @classmethod
82    def get_unique_iml_name(cls, abs_module_path):
83        """Create a unique iml name if needed.
84
85        If the name of last sub folder is used already, prefixing it with prior
86        sub folder names as a candidate name. If finally, it's unique, storing
87        in USED_NAME_CACHE as: { abs_module_path:unique_name }. The cts case
88        and UX of IDE view are the main reasons why using module path strategy
89        but not name of module directly. Following is the detailed strategy:
90        1. While loop composes a sensible and shorter name, by checking unique
91           to finish the loop and finally add to cache.
92           Take ['cts', 'tests', 'app', 'ui'] an example, if 'ui' isn't
93           occupied, use it, else try 'cts_ui', then 'cts_app_ui', the worst
94           case is whole three candidate names are occupied already.
95        2. 'Else' for that while stands for no suitable name generated, so
96           trying 'cts_tests_app_ui' directly. If it's still non unique, e.g.,
97           module path cts/xxx/tests/app/ui occupied that name already,
98           appending increasing sequence number to get a unique name.
99
100        Args:
101            abs_module_path: The absolute module path string.
102
103        Return:
104            String: A unique iml name.
105        """
106        if abs_module_path in cls.USED_NAME_CACHE:
107            return cls.USED_NAME_CACHE[abs_module_path]
108
109        uniq_name = abs_module_path.strip(os.sep).split(os.sep)[-1]
110        if any(uniq_name == name for name in cls.USED_NAME_CACHE.values()):
111            parent_path = os.path.relpath(abs_module_path,
112                                          common_util.get_android_root_dir())
113            sub_folders = parent_path.split(os.sep)
114            zero_base_index = len(sub_folders) - 1
115            # Start compose a sensible, shorter and unique name.
116            while zero_base_index > 0:
117                uniq_name = '_'.join(
118                    [sub_folders[0], '_'.join(sub_folders[zero_base_index:])])
119                zero_base_index = zero_base_index - 1
120                if uniq_name not in cls.USED_NAME_CACHE.values():
121                    break
122            else:
123                # TODO(b/133393638): To handle several corner cases.
124                uniq_name_base = parent_path.strip(os.sep).replace(os.sep, '_')
125                i = 0
126                uniq_name = uniq_name_base
127                while uniq_name in cls.USED_NAME_CACHE.values():
128                    i = i + 1
129                    uniq_name = '_'.join([uniq_name_base, str(i)])
130        cls.USED_NAME_CACHE[abs_module_path] = uniq_name
131        logging.debug('Unique name for module path of %s is %s.',
132                      abs_module_path, uniq_name)
133        return uniq_name
134
135    @property
136    def iml_path(self):
137        """Gets the iml path."""
138        return self._iml_path
139
140    def create(self, content_type):
141        """Creates the iml file.
142
143        Create the iml file with specific part of sources.
144        e.g.
145        {
146            'srcs': True,
147            'dependencies': True,
148        }
149
150        Args:
151            content_type: A dict to set which part of sources will be created.
152        """
153        if content_type.get(constant.KEY_SRCS, None):
154            self._generate_srcs()
155        if content_type.get(constant.KEY_DEP_SRCS, None):
156            self._generate_dep_srcs()
157        if content_type.get(constant.KEY_JARS, None):
158            self._generate_jars()
159        if content_type.get(constant.KEY_SRCJARS, None):
160            self._generate_srcjars()
161        if content_type.get(constant.KEY_DEPENDENCIES, None):
162            self._generate_dependencies()
163
164        if self._srcs or self._jars or self._srcjars or self._deps:
165            self._create_iml()
166
167    def _generate_facet(self):
168        """Generates the facet when the AndroidManifest.xml exists."""
169        if os.path.exists(os.path.join(self._mod_path,
170                                       constant.ANDROID_MANIFEST)):
171            self._facet = templates.FACET
172
173    def _generate_srcs(self):
174        """Generates the source urls of the project's iml file."""
175        srcs = []
176        for src in self._mod_info[constant.KEY_SRCS]:
177            srcs.append(templates.SOURCE.format(
178                SRC=os.path.join(self._android_root, src),
179                IS_TEST='false'))
180        for test in self._mod_info[constant.KEY_TESTS]:
181            srcs.append(templates.SOURCE.format(
182                SRC=os.path.join(self._android_root, test),
183                IS_TEST='true'))
184        self._excludes = self._mod_info.get(constant.KEY_EXCLUDES, '')
185        self._srcs = templates.CONTENT.format(MODULE_PATH=self._mod_path,
186                                              EXCLUDES=self._excludes,
187                                              SOURCES=''.join(sorted(srcs)))
188
189    def _generate_dep_srcs(self):
190        """Generates the source urls of the dependencies.iml."""
191        srcs = []
192        for src in self._mod_info[constant.KEY_SRCS]:
193            srcs.append(templates.OTHER_SOURCE.format(
194                SRC=os.path.join(self._android_root, src),
195                IS_TEST='false'))
196        for test in self._mod_info[constant.KEY_TESTS]:
197            srcs.append(templates.OTHER_SOURCE.format(
198                SRC=os.path.join(self._android_root, test),
199                IS_TEST='true'))
200        self._srcs = ''.join(sorted(srcs))
201
202    def _generate_jars(self):
203        """Generates the jar urls."""
204        for jar in self._mod_info[constant.KEY_JARS]:
205            self._jars.append(templates.JAR.format(
206                JAR=os.path.join(self._android_root, jar)))
207
208    def _generate_srcjars(self):
209        """Generates the srcjar urls."""
210        for srcjar in self._mod_info[constant.KEY_SRCJARS]:
211            self._srcjars.append(templates.SRCJAR.format(
212                SRCJAR=os.path.join(self._android_root, srcjar)))
213
214    def _generate_dependencies(self):
215        """Generates the dependency module urls."""
216        for dep in self._mod_info[constant.KEY_DEPENDENCIES]:
217            self._deps.append(templates.DEPENDENCIES.format(MODULE=dep))
218
219    def _create_iml(self):
220        """Creates the iml file."""
221        content = templates.IML.format(FACET=self._facet,
222                                       SOURCES=self._srcs,
223                                       JARS=''.join(self._jars),
224                                       SRCJARS=''.join(self._srcjars),
225                                       DEPENDENCIES=''.join(self._deps))
226        common_util.file_generate(self._iml_path, content)
227