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                logging.info('\nThe batch_build_dependencies function is '
372                             'called by ProjectInfo\'s locate_source method.')
373                batch_build_dependencies(rebuild_targets)
374                self.locate_source(build=False)
375            else:
376                logging.warning('Jar or srcjar files build skipped:\n\t%s.',
377                                '\n\t'.join(rebuild_targets))
378
379    def _generate_moduledata(self, module_name, module_data):
380        """Generate a module class to collect dependencies in IDE.
381
382        The rules of initialize a module data instance: if ide_object isn't None
383        and its ide_name is 'eclipse', we'll create an EclipseModuleData
384        instance otherwise create a ModuleData instance.
385
386        Args:
387            module_name: Name of the module.
388            module_data: A dictionary holding a module information.
389
390        Returns:
391            A ModuleData class.
392        """
393        ide_name = project_config.ProjectConfig.get_instance().ide_name
394        if ide_name == constant.IDE_ECLIPSE:
395            return source_locator.EclipseModuleData(
396                module_name, module_data, self.project_relative_path)
397        depth = project_config.ProjectConfig.get_instance().depth
398        return source_locator.ModuleData(module_name, module_data, depth)
399
400    def _append_jars_as_dependencies(self, module):
401        """Add given module's jar files into dependent_data as dependencies.
402
403        Args:
404            module: A ModuleData instance.
405        """
406        if module.jar_files:
407            self.source_path['jar_path'].update(module.jar_files)
408            for jar in list(module.jar_files):
409                self.source_path['jar_module_path'].update({
410                    jar:
411                    module.module_path
412                })
413        # Collecting the jar files of default core modules as dependencies.
414        if constant.KEY_DEPENDENCIES in module.module_data:
415            self.source_path['jar_path'].update([
416                x for x in module.module_data[constant.KEY_DEPENDENCIES]
417                if common_util.is_target(x, constant.TARGET_LIBS)
418            ])
419
420    @classmethod
421    def multi_projects_locate_source(cls, projects):
422        """Locate the paths of dependent source folders and jar files.
423
424        Args:
425            projects: A list of ProjectInfo instances. Information of a project
426                      such as project relative path, project real path, project
427                      dependencies.
428        """
429        for project in projects:
430            project.locate_source()
431
432
433class MultiProjectsInfo(ProjectInfo):
434    """Multiple projects info.
435
436    Usage example:
437        if folder_base:
438            project = MultiProjectsInfo(['module_name'])
439            project.collect_all_dep_modules()
440            project.gen_folder_base_dependencies()
441        else:
442            ProjectInfo.generate_projects(['module_name'])
443
444    Attributes:
445        _targets: A list of module names or project paths.
446        path_to_sources: A dictionary of modules' sources, the module's path
447                         as key and the sources as value.
448                         e.g.
449                         {
450                             'frameworks/base': {
451                                 'src_dirs': [],
452                                 'test_dirs': [],
453                                 'r_java_paths': [],
454                                 'srcjar_paths': [],
455                                 'jar_files': [],
456                                 'dep_paths': [],
457                             }
458                         }
459    """
460
461    def __init__(self, targets=None):
462        """MultiProjectsInfo initialize.
463
464        Args:
465            targets: A list of module names or project paths from user's input.
466        """
467        super().__init__(targets[0], True)
468        self._targets = targets
469        self.path_to_sources = {}
470
471    def _clear_srcjar_paths(self, module):
472        """Clears the srcjar_paths.
473
474        Args:
475            module: A ModuleData instance.
476        """
477        module.srcjar_paths = []
478
479    def _collect_framework_srcjar_info(self, module):
480        """Clears the framework's srcjars.
481
482        Args:
483            module: A ModuleData instance.
484        """
485        if module.module_path == constant.FRAMEWORK_PATH:
486            framework_srcjar_path = os.path.join(constant.FRAMEWORK_PATH,
487                                                 constant.FRAMEWORK_SRCJARS)
488            if module.module_name == constant.FRAMEWORK_ALL:
489                self.path_to_sources[framework_srcjar_path] = {
490                    'src_dirs': [],
491                    'test_dirs': [],
492                    'r_java_paths': [],
493                    'srcjar_paths': module.srcjar_paths,
494                    'jar_files': [],
495                    'dep_paths': [constant.FRAMEWORK_PATH],
496                }
497            # In the folder base case, AIDEGen has to ignore all module's srcjar
498            # files under the frameworks/base except the framework-all. Because
499            # there are too many duplicate srcjars of modules under the
500            # frameworks/base. So that AIDEGen keeps the srcjar files only from
501            # the framework-all module. Other modeuls' srcjar files will be
502            # removed. However, when users choose the module base case, srcjar
503            # files will be collected by the ProjectInfo class, so that the
504            # removing srcjar_paths in this class does not impact the
505            # srcjar_paths collection of modules in the ProjectInfo class.
506            self._clear_srcjar_paths(module)
507
508    def collect_all_dep_modules(self):
509        """Collects all dependency modules for the projects."""
510        self.project_module_names.clear()
511        module_names = set(_CORE_MODULES)
512        for target in self._targets:
513            relpath, _ = common_util.get_related_paths(self.modules_info,
514                                                       target)
515            module_names.update(self._get_modules_under_project_path(relpath))
516        module_names.update(self._get_robolectric_dep_module(module_names))
517        self.dep_modules = self.get_dep_modules(module_names)
518
519    def gen_folder_base_dependencies(self, module):
520        """Generates the folder base dependencies dictionary.
521
522        Args:
523            module: A ModuleData instance.
524        """
525        mod_path = module.module_path
526        if not mod_path:
527            logging.debug('The %s\'s path is empty.', module.module_name)
528            return
529        self._collect_framework_srcjar_info(module)
530        if mod_path not in self.path_to_sources:
531            self.path_to_sources[mod_path] = {
532                'src_dirs': module.src_dirs,
533                'test_dirs': module.test_dirs,
534                'r_java_paths': module.r_java_paths,
535                'srcjar_paths': module.srcjar_paths,
536                'jar_files': module.jar_files,
537                'dep_paths': module.dep_paths,
538            }
539        else:
540            for key, val in self.path_to_sources[mod_path].items():
541                val.extend([v for v in getattr(module, key) if v not in val])
542
543
544def batch_build_dependencies(rebuild_targets):
545    """Batch build the jar or srcjar files of the modules if they don't exist.
546
547    Command line has the max length limit, MAX_ARG_STRLEN, and
548    MAX_ARG_STRLEN = (PAGE_SIZE * 32).
549    If the build command is longer than MAX_ARG_STRLEN, this function will
550    separate the rebuild_targets into chunks with size less or equal to
551    MAX_ARG_STRLEN to make sure it can be built successfully.
552
553    Args:
554        rebuild_targets: A set of jar or srcjar files which do not exist.
555    """
556    logging.info('Ready to build the jar or srcjar files. Files count = %s',
557                 str(len(rebuild_targets)))
558    arg_max = os.sysconf('SC_PAGE_SIZE') * 32 - _CMD_LENGTH_BUFFER
559    rebuild_targets = list(rebuild_targets)
560    for start, end in iter(_separate_build_targets(rebuild_targets, arg_max)):
561        _build_target(rebuild_targets[start:end])
562
563
564def _build_target(targets):
565    """Build the jar or srcjar files.
566
567    Use -k to keep going when some targets can't be built or build failed.
568    Use -j to speed up building.
569
570    Args:
571        targets: A list of jar or srcjar files which need to build.
572    """
573    build_cmd = ['-k', '-j']
574    build_cmd.extend(list(targets))
575    verbose = True
576    if not atest_utils.build(build_cmd, verbose):
577        message = ('Build failed!\n{}\nAIDEGen will proceed but dependency '
578                   'correctness is not guaranteed if not all targets being '
579                   'built successfully.'.format('\n'.join(targets)))
580        print('\n{} {}\n'.format(common_util.COLORED_INFO('Warning:'), message))
581
582
583def _separate_build_targets(build_targets, max_length):
584    """Separate the build_targets by limit the command size to max command
585    length.
586
587    Args:
588        build_targets: A list to be separated.
589        max_length: The max number of each build command length.
590
591    Yields:
592        The start index and end index of build_targets.
593    """
594    arg_len = 0
595    first_item_index = 0
596    for i, item in enumerate(build_targets):
597        arg_len = arg_len + len(item) + _BLANK_SIZE
598        if arg_len > max_length:
599            yield first_item_index, i
600            first_item_index = i
601            arg_len = len(item) + _BLANK_SIZE
602    if first_item_index < len(build_targets):
603        yield first_item_index, len(build_targets)
604