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"""Project information."""
18
19from __future__ import absolute_import
20
21import logging
22import os
23
24from aidegen import constant
25from aidegen.lib import common_util
26from aidegen.lib import errors
27from aidegen.lib import module_info
28from aidegen.lib import project_config
29from aidegen.lib import source_locator
30
31from atest import atest_utils
32
33_CONVERT_MK_URL = ('https://android.googlesource.com/platform/build/soong/'
34                   '#convert-android_mk-files')
35_ANDROID_MK_WARN = (
36    '{} contains Android.mk file(s) in its dependencies:\n{}\nPlease help '
37    'convert these files into blueprint format in the future, otherwise '
38    'AIDEGen may not be able to include all module dependencies.\nPlease visit '
39    '%s for reference on how to convert makefile.' % _CONVERT_MK_URL)
40_ROBOLECTRIC_MODULE = 'Robolectric_all'
41_NOT_TARGET = ('Module %s\'s class setting is %s, none of which is included in '
42               '%s, skipping this module in the project.')
43# The module fake-framework have the same package name with framework but empty
44# content. It will impact the dependency for framework when referencing the
45# package from fake-framework in IntelliJ.
46_EXCLUDE_MODULES = ['fake-framework']
47# When we use atest_utils.build(), there is a command length limit on
48# soong_ui.bash. We reserve 5000 characters for rewriting the command line
49# in soong_ui.bash.
50_CMD_LENGTH_BUFFER = 5000
51# For each argument, it need a space to separate following argument.
52_BLANK_SIZE = 1
53_CORE_MODULES = [constant.FRAMEWORK_ALL, constant.CORE_ALL,
54                 'org.apache.http.legacy.stubs.system']
55
56
57class ProjectInfo:
58    """Project information.
59
60    Users should call config_project first before starting using ProjectInfo.
61
62    Class attributes:
63        modules_info: An AidegenModuleInfo instance whose name_to_module_info is
64                      combining module-info.json with module_bp_java_deps.json.
65
66    Attributes:
67        project_absolute_path: The absolute path of the project.
68        project_relative_path: The relative path of the project to
69                               common_util.get_android_root_dir().
70        project_module_names: A set of module names under project_absolute_path
71                              directory or it's subdirectories.
72        dep_modules: A dict has recursively dependent modules of
73                     project_module_names.
74        iml_path: The project's iml file path.
75        source_path: A dictionary to keep following data:
76                     source_folder_path: A set contains the source folder
77                                         relative paths.
78                     test_folder_path: A set contains the test folder relative
79                                       paths.
80                     jar_path: A set contains the jar file paths.
81                     jar_module_path: A dictionary contains the jar file and
82                                      the module's path mapping, only used in
83                                      Eclipse.
84                     r_java_path: A set contains the relative path to the
85                                  R.java files, only used in Eclipse.
86                     srcjar_path: A source content descriptor only used in
87                                  IntelliJ.
88                                  e.g. out/.../aapt2.srcjar!/
89                                  The "!/" is a content descriptor for
90                                  compressed files in IntelliJ.
91        is_main_project: A boolean to verify the project is main project.
92        dependencies: A list of dependency projects' iml file names, e.g. base,
93                      framework-all.
94    """
95
96    modules_info = None
97
98    def __init__(self, target=None, is_main_project=False):
99        """ProjectInfo initialize.
100
101        Args:
102            target: Includes target module or project path from user input, when
103                    locating the target, project with matching module name of
104                    the given target has a higher priority than project path.
105            is_main_project: A boolean, default is False. True if the target is
106                             the main project, otherwise False.
107        """
108        rel_path, abs_path = common_util.get_related_paths(
109            self.modules_info, target)
110        self.module_name = self.get_target_name(target, abs_path)
111        self.is_main_project = is_main_project
112        self.project_module_names = set(
113            self.modules_info.get_module_names(rel_path))
114        self.project_relative_path = rel_path
115        self.project_absolute_path = abs_path
116        self.iml_path = ''
117        self._set_default_modues()
118        self._init_source_path()
119        if target == constant.FRAMEWORK_ALL:
120            self.dep_modules = self.get_dep_modules([target])
121        else:
122            self.dep_modules = self.get_dep_modules()
123        self._filter_out_modules()
124        self._display_convert_make_files_message()
125        self.dependencies = []
126
127    def _set_default_modues(self):
128        """Append default hard-code modules, source paths and jar files.
129
130        1. framework: Framework module is always needed for dependencies but it
131            might not always be located by module dependency.
132        2. org.apache.http.legacy.stubs.system: The module can't be located
133            through module dependency. Without it, a lot of java files will have
134            error of "cannot resolve symbol" in IntelliJ since they import
135            packages android.Manifest and com.android.internal.R.
136        """
137        # Set the default modules framework-all and core-all as the core
138        # dependency modules.
139        self.project_module_names.update(_CORE_MODULES)
140
141    def _init_source_path(self):
142        """Initialize source_path dictionary."""
143        self.source_path = {
144            'source_folder_path': set(),
145            'test_folder_path': set(),
146            'jar_path': set(),
147            'jar_module_path': dict(),
148            'r_java_path': set(),
149            'srcjar_path': set()
150        }
151
152    def _display_convert_make_files_message(self):
153        """Show message info users convert their Android.mk to Android.bp."""
154        mk_set = set(self._search_android_make_files())
155        if mk_set:
156            print('\n{} {}\n'.format(
157                common_util.COLORED_INFO('Warning:'),
158                _ANDROID_MK_WARN.format(self.module_name, '\n'.join(mk_set))))
159
160    def _search_android_make_files(self):
161        """Search project and dependency modules contain Android.mk files.
162
163        If there is only Android.mk but no Android.bp, we'll show the warning
164        message, otherwise we won't.
165
166        Yields:
167            A string: the relative path of Android.mk.
168        """
169        if (common_util.exist_android_mk(self.project_absolute_path) and
170                not common_util.exist_android_bp(self.project_absolute_path)):
171            yield '\t' + os.path.join(self.project_relative_path,
172                                      constant.ANDROID_MK)
173        for mod_name in self.dep_modules:
174            rel_path, abs_path = common_util.get_related_paths(
175                self.modules_info, mod_name)
176            if rel_path and abs_path:
177                if (common_util.exist_android_mk(abs_path)
178                        and not common_util.exist_android_bp(abs_path)):
179                    yield '\t' + os.path.join(rel_path, constant.ANDROID_MK)
180
181    def _get_modules_under_project_path(self, rel_path):
182        """Find modules under the rel_path.
183
184        Find modules whose class is qualified to be included as a target module.
185
186        Args:
187            rel_path: A string, the project's relative path.
188
189        Returns:
190            A set of module names.
191        """
192        logging.info('Find modules whose class is in %s under %s.',
193                     constant.TARGET_CLASSES, rel_path)
194        modules = set()
195        for name, data in self.modules_info.name_to_module_info.items():
196            if module_info.AidegenModuleInfo.is_project_path_relative_module(
197                    data, rel_path):
198                if module_info.AidegenModuleInfo.is_target_module(data):
199                    modules.add(name)
200                else:
201                    logging.debug(_NOT_TARGET, name, data.get('class', ''),
202                                  constant.TARGET_CLASSES)
203        return modules
204
205    def _get_robolectric_dep_module(self, modules):
206        """Return the robolectric module set as dependency if any module is a
207           robolectric test.
208
209        Args:
210            modules: A set of modules.
211
212        Returns:
213            A set with a robolectric_all module name if one of the modules
214            needs the robolectric test module. Otherwise return empty list.
215        """
216        for module in modules:
217            if self.modules_info.is_robolectric_test(module):
218                return {_ROBOLECTRIC_MODULE}
219        return set()
220
221    def _filter_out_modules(self):
222        """Filter out unnecessary modules."""
223        for module in _EXCLUDE_MODULES:
224            self.dep_modules.pop(module, None)
225
226    def get_dep_modules(self, module_names=None, depth=0):
227        """Recursively find dependent modules of the project.
228
229        Find dependent modules by dependencies parameter of each module.
230        For example:
231            The module_names is ['m1'].
232            The modules_info is
233            {
234                'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']},
235                'm2': {'path': ['path_to_m4']},
236                'm3': {'path': ['path_to_m1']}
237                'm4': {'path': []}
238            }
239            The result dependent modules are:
240            {
241                'm1': {'dependencies': ['m2'], 'path': ['path_to_m1']
242                       'depth': 0},
243                'm2': {'path': ['path_to_m4'], 'depth': 1},
244                'm3': {'path': ['path_to_m1'], 'depth': 0}
245            }
246            Note that:
247                1. m4 is not in the result as it's not among dependent modules.
248                2. m3 is in the result as it has the same path to m1.
249
250        Args:
251            module_names: A set of module names.
252            depth: An integer shows the depth of module dependency referenced by
253                   source. Zero means the max module depth.
254
255        Returns:
256            deps: A dict contains all dependent modules data of given modules.
257        """
258        dep = {}
259        children = set()
260        if not module_names:
261            module_names = self.project_module_names
262            module_names.update(
263                self._get_modules_under_project_path(
264                    self.project_relative_path))
265            module_names.update(self._get_robolectric_dep_module(module_names))
266            self.project_module_names = set()
267        for name in module_names:
268            if (name in self.modules_info.name_to_module_info
269                    and name not in self.project_module_names):
270                dep[name] = self.modules_info.name_to_module_info[name]
271                dep[name][constant.KEY_DEPTH] = depth
272                self.project_module_names.add(name)
273                if (constant.KEY_DEPENDENCIES in dep[name]
274                        and dep[name][constant.KEY_DEPENDENCIES]):
275                    children.update(dep[name][constant.KEY_DEPENDENCIES])
276        if children:
277            dep.update(self.get_dep_modules(children, depth + 1))
278        return dep
279
280    @staticmethod
281    def generate_projects(targets):
282        """Generate a list of projects in one time by a list of module names.
283
284        Args:
285            targets: A list of target modules or project paths from user input,
286                     when locating the target, project with matched module name
287                     of the target has a higher priority than project path.
288
289        Returns:
290            List: A list of ProjectInfo instances.
291        """
292        return [ProjectInfo(target, i == 0) for i, target in enumerate(targets)]
293
294    @staticmethod
295    def get_target_name(target, abs_path):
296        """Get target name from target's absolute path.
297
298        If the project is for entire Android source tree, change the target to
299        source tree's root folder name. In this way, we give IDE project file
300        a more specific name. e.g, master.iml.
301
302        Args:
303            target: Includes target module or project path from user input, when
304                    locating the target, project with matching module name of
305                    the given target has a higher priority than project path.
306            abs_path: A string, target's absolute path.
307
308        Returns:
309            A string, the target name.
310        """
311        if abs_path == common_util.get_android_root_dir():
312            return os.path.basename(abs_path)
313        return target
314
315    def locate_source(self, build=True):
316        """Locate the paths of dependent source folders and jar files.
317
318        Try to reference source folder path as dependent module unless the
319        dependent module should be referenced to a jar file, such as modules
320        have jars and jarjar_rules parameter.
321        For example:
322            Module: asm-6.0
323                java_import {
324                    name: 'asm-6.0',
325                    host_supported: true,
326                    jars: ['asm-6.0.jar'],
327                }
328            Module: bouncycastle
329                java_library {
330                    name: 'bouncycastle',
331                    ...
332                    target: {
333                        android: {
334                            jarjar_rules: 'jarjar-rules.txt',
335                        },
336                    },
337                }
338
339        Args:
340            build: A boolean default to true. If false, skip building jar and
341                   srcjar files, otherwise build them.
342
343        Example usage:
344            project.source_path = project.locate_source()
345            E.g.
346                project.source_path = {
347                    'source_folder_path': ['path/to/source/folder1',
348                                           'path/to/source/folder2', ...],
349                    'test_folder_path': ['path/to/test/folder', ...],
350                    'jar_path': ['path/to/jar/file1', 'path/to/jar/file2', ...]
351                }
352        """
353        if not hasattr(self, 'dep_modules') or not self.dep_modules:
354            raise errors.EmptyModuleDependencyError(
355                'Dependent modules dictionary is empty.')
356        rebuild_targets = set()
357        for module_name, module_data in self.dep_modules.items():
358            module = self._generate_moduledata(module_name, module_data)
359            module.locate_sources_path()
360            self.source_path['source_folder_path'].update(set(module.src_dirs))
361            self.source_path['test_folder_path'].update(set(module.test_dirs))
362            self.source_path['r_java_path'].update(set(module.r_java_paths))
363            self.source_path['srcjar_path'].update(set(module.srcjar_paths))
364            self._append_jars_as_dependencies(module)
365            rebuild_targets.update(module.build_targets)
366        config = project_config.ProjectConfig.get_instance()
367        if config.is_skip_build:
368            return
369        if rebuild_targets:
370            if build:
371                batch_build_dependencies(rebuild_targets)
372                self.locate_source(build=False)
373            else:
374                logging.warning('Jar or srcjar files build skipped:\n\t%s.',
375                                '\n\t'.join(rebuild_targets))
376
377    def _generate_moduledata(self, module_name, module_data):
378        """Generate a module class to collect dependencies in IDE.
379
380        The rules of initialize a module data instance: if ide_object isn't None
381        and its ide_name is 'eclipse', we'll create an EclipseModuleData
382        instance otherwise create a ModuleData instance.
383
384        Args:
385            module_name: Name of the module.
386            module_data: A dictionary holding a module information.
387
388        Returns:
389            A ModuleData class.
390        """
391        ide_name = project_config.ProjectConfig.get_instance().ide_name
392        if ide_name == constant.IDE_ECLIPSE:
393            return source_locator.EclipseModuleData(
394                module_name, module_data, self.project_relative_path)
395        depth = project_config.ProjectConfig.get_instance().depth
396        return source_locator.ModuleData(module_name, module_data, depth)
397
398    def _append_jars_as_dependencies(self, module):
399        """Add given module's jar files into dependent_data as dependencies.
400
401        Args:
402            module: A ModuleData instance.
403        """
404        if module.jar_files:
405            self.source_path['jar_path'].update(module.jar_files)
406            for jar in list(module.jar_files):
407                self.source_path['jar_module_path'].update({
408                    jar:
409                    module.module_path
410                })
411        # Collecting the jar files of default core modules as dependencies.
412        if constant.KEY_DEPENDENCIES in module.module_data:
413            self.source_path['jar_path'].update([
414                x for x in module.module_data[constant.KEY_DEPENDENCIES]
415                if common_util.is_target(x, constant.TARGET_LIBS)
416            ])
417
418    @classmethod
419    def multi_projects_locate_source(cls, projects):
420        """Locate the paths of dependent source folders and jar files.
421
422        Args:
423            projects: A list of ProjectInfo instances. Information of a project
424                      such as project relative path, project real path, project
425                      dependencies.
426        """
427        for project in projects:
428            project.locate_source()
429
430
431class MultiProjectsInfo(ProjectInfo):
432    """Multiple projects info.
433
434    Usage example:
435        project = MultiProjectsInfo(['module_name'])
436        project.collect_all_dep_modules()
437    """
438
439    def __init__(self, targets=None):
440        """MultiProjectsInfo initialize.
441
442        Args:
443            targets: A list of module names or project paths from user's input.
444        """
445        super().__init__(targets[0], True)
446        self._targets = targets
447
448    def collect_all_dep_modules(self):
449        """Collects all dependency modules for the projects."""
450        self.project_module_names = set()
451        module_names = set(_CORE_MODULES)
452        for target in self._targets:
453            relpath, _ = common_util.get_related_paths(self.modules_info,
454                                                       target)
455            module_names.update(self._get_modules_under_project_path(relpath))
456        module_names.update(self._get_robolectric_dep_module(module_names))
457        self.dep_modules = self.get_dep_modules(module_names)
458
459
460def batch_build_dependencies(rebuild_targets):
461    """Batch build the jar or srcjar files of the modules if they don't exist.
462
463    Command line has the max length limit, MAX_ARG_STRLEN, and
464    MAX_ARG_STRLEN = (PAGE_SIZE * 32).
465    If the build command is longer than MAX_ARG_STRLEN, this function will
466    separate the rebuild_targets into chunks with size less or equal to
467    MAX_ARG_STRLEN to make sure it can be built successfully.
468
469    Args:
470        rebuild_targets: A set of jar or srcjar files which do not exist.
471    """
472    logging.info('Ready to build the jar or srcjar files. Files count = %s',
473                 str(len(rebuild_targets)))
474    arg_max = os.sysconf('SC_PAGE_SIZE') * 32 - _CMD_LENGTH_BUFFER
475    rebuild_targets = list(rebuild_targets)
476    for start, end in iter(_separate_build_targets(rebuild_targets, arg_max)):
477        _build_target(rebuild_targets[start:end])
478
479
480def _build_target(targets):
481    """Build the jar or srcjar files.
482
483    Use -k to keep going when some targets can't be built or build failed.
484    Use -j to speed up building.
485
486    Args:
487        targets: A list of jar or srcjar files which need to build.
488    """
489    build_cmd = ['-k', '-j']
490    build_cmd.extend(list(targets))
491    verbose = True
492    if not atest_utils.build(build_cmd, verbose):
493        message = ('Build failed!\n{}\nAIDEGen will proceed but dependency '
494                   'correctness is not guaranteed if not all targets being '
495                   'built successfully.'.format('\n'.join(targets)))
496        print('\n{} {}\n'.format(common_util.COLORED_INFO('Warning:'), message))
497
498
499def _separate_build_targets(build_targets, max_length):
500    """Separate the build_targets by limit the command size to max command
501    length.
502
503    Args:
504        build_targets: A list to be separated.
505        max_length: The max number of each build command length.
506
507    Yields:
508        The start index and end index of build_targets.
509    """
510    arg_len = 0
511    first_item_index = 0
512    for i, item in enumerate(build_targets):
513        arg_len = arg_len + len(item) + _BLANK_SIZE
514        if arg_len > max_length:
515            yield first_item_index, i
516            first_item_index = i
517            arg_len = len(item) + _BLANK_SIZE
518    if first_item_index < len(build_targets):
519        yield first_item_index, len(build_targets)
520