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"""module_info_util
18
19This module receives a module path which is relative to its root directory and
20makes a command to generate two json files, one for mk files and one for bp
21files. Then it will load these two json files into two json dictionaries,
22merge them into one dictionary and return the merged dictionary to its caller.
23
24Example usage:
25merged_dict = generate_merged_module_info()
26"""
27
28import glob
29import logging
30import os
31import sys
32
33from aidegen import constant
34from aidegen.lib import common_util
35from aidegen.lib import errors
36from aidegen.lib import project_config
37
38from atest import atest_utils
39
40_MERGE_NEEDED_ITEMS = [
41    constant.KEY_CLASS,
42    constant.KEY_PATH,
43    constant.KEY_INSTALLED,
44    constant.KEY_DEPENDENCIES,
45    constant.KEY_SRCS,
46    constant.KEY_SRCJARS,
47    constant.KEY_CLASSES_JAR,
48    constant.KEY_TAG,
49    constant.KEY_COMPATIBILITY,
50    constant.KEY_AUTO_TEST_CONFIG,
51    constant.KEY_MODULE_NAME,
52    constant.KEY_TEST_CONFIG
53]
54_INTELLIJ_PROJECT_FILE_EXT = '*.iml'
55_LAUNCH_PROJECT_QUERY = (
56    'There exists an IntelliJ project file: %s. Do you want '
57    'to launch it (yes/No)?')
58_BUILD_BP_JSON_ENV_OFF = {
59    constant.GEN_JAVA_DEPS: 'false',
60    constant.GEN_CC_DEPS: 'false',
61    constant.GEN_COMPDB: 'false'
62}
63_BUILD_BP_JSON_ENV_ON = {
64    constant.GEN_JAVA_DEPS: 'true',
65    constant.GEN_CC_DEPS: 'true',
66    constant.GEN_COMPDB: 'true'
67}
68_GEN_JSON_FAILED = (
69    'Generate new {0} failed, AIDEGen will proceed and reuse the old {1}.')
70_WARN_MSG = '\n{} {}\n'
71_TARGET = 'nothing'
72
73
74# pylint: disable=dangerous-default-value
75@common_util.back_to_cwd
76@common_util.time_logged
77def generate_merged_module_info(env_off=_BUILD_BP_JSON_ENV_OFF,
78                                env_on=_BUILD_BP_JSON_ENV_ON):
79    """Generate a merged dictionary.
80
81    Linked functions:
82        _build_bp_info(module_info, project, verbose, skip_build)
83        _get_soong_build_json_dict()
84        _merge_dict(mk_dict, bp_dict)
85
86    Args:
87        env_off: A dictionary of environment settings to be turned off, the
88                 default value is _BUILD_BP_JSON_ENV_OFF.
89        env_on: A dictionary of environment settings to be turned on, the
90                default value is _BUILD_BP_JSON_ENV_ON.
91
92    Returns:
93        A merged dictionary from module-info.json and module_bp_java_deps.json.
94    """
95    config = project_config.ProjectConfig.get_instance()
96    module_info = config.atest_module_info
97    projects = config.targets
98    verbose = True
99    skip_build = config.is_skip_build
100    main_project = projects[0] if projects else None
101    _build_bp_info(
102        module_info, main_project, verbose, skip_build, env_off, env_on)
103    json_path = common_util.get_blueprint_json_path(
104        constant.BLUEPRINT_JAVA_JSONFILE_NAME)
105    bp_dict = common_util.get_json_dict(json_path)
106    return _merge_dict(module_info.name_to_module_info, bp_dict)
107
108
109def _build_bp_info(module_info, main_project=None, verbose=False,
110                   skip_build=False, env_off=_BUILD_BP_JSON_ENV_OFF,
111                   env_on=_BUILD_BP_JSON_ENV_ON):
112    """Make nothing to create module_bp_java_deps.json, module_bp_cc_deps.json.
113
114    Use atest build method to build the target 'nothing' by setting env config
115    SOONG_COLLECT_JAVA_DEPS to false then true. By this way, we can trigger the
116    process of collecting dependencies and generate module_bp_java_deps.json.
117
118    Args:
119        module_info: A ModuleInfo instance contains data of module-info.json.
120        main_project: A string of the main project name.
121        verbose: A boolean, if true displays full build output.
122        skip_build: A boolean, if true, skip building if
123                    get_blueprint_json_path(file_name) file exists, otherwise
124                    build it.
125        env_off: A dictionary of environment settings to be turned off, the
126                 default value is _BUILD_BP_JSON_ENV_OFF.
127        env_on: A dictionary of environment settings to be turned on, the
128                default value is _BUILD_BP_JSON_ENV_ON.
129
130    Build results:
131        1. Build successfully return.
132        2. Build failed:
133           1) There's no project file, raise BuildFailureError.
134           2) There exists a project file, ask users if they want to
135              launch IDE with the old project file.
136              a) If the answer is yes, return.
137              b) If the answer is not yes, sys.exit(1)
138    """
139    file_paths = _get_generated_json_files(env_on)
140    files_exist = all([os.path.isfile(fpath) for fpath in file_paths])
141    files = '\n'.join(file_paths)
142    if skip_build and files_exist:
143        logging.info('Files:\n%s exist, skipping build.', files)
144        return
145    original_file_mtimes = {f: None for f in file_paths}
146    if files_exist:
147        original_file_mtimes = {f: os.path.getmtime(f) for f in file_paths}
148
149    logging.warning(
150        '\nGenerate files:\n %s by atest build method.', files)
151    build_with_off_cmd = atest_utils.build([_TARGET], verbose, env_off)
152    build_with_on_cmd = atest_utils.build([_TARGET], verbose, env_on)
153
154    if build_with_off_cmd and build_with_on_cmd:
155        logging.info('\nGenerate blueprint json successfully.')
156    else:
157        if not all([_is_new_json_file_generated(
158                f, original_file_mtimes[f]) for f in file_paths]):
159            if files_exist:
160                _show_files_reuse_message(file_paths)
161            else:
162                _show_build_failed_message(module_info, main_project)
163
164
165def _get_generated_json_files(env_on=_BUILD_BP_JSON_ENV_ON):
166    """Gets the absolute paths of the files which is going to be generated.
167
168    Determine the files which will be generated by the environment on dictionary
169    and the default blueprint json files' dictionary.
170    The generation of json files depends on env_on. If the env_on looks like,
171    _BUILD_BP_JSON_ENV_ON = {
172        'SOONG_COLLECT_JAVA_DEPS': 'true',
173        'SOONG_COLLECT_CC_DEPS': 'true'
174    }
175    We want to generate only two files: module_bp_java_deps.json and
176    module_bp_cc_deps.json. And in get_blueprint_json_files_relative_dict
177    function, there are three json files by default. We get the result list by
178    comparsing with these two dictionaries.
179
180    Args:
181        env_on: A dictionary of environment settings to be turned on, the
182                default value is _BUILD_BP_JSON_ENV_ON.
183
184    Returns:
185        A list of the absolute paths of the files which is going to be
186        generated.
187    """
188    json_files_dict = common_util.get_blueprint_json_files_relative_dict()
189    file_paths = []
190    for key in env_on:
191        if not env_on[key] == 'true' or key not in json_files_dict:
192            continue
193        file_paths.append(json_files_dict[key])
194    return file_paths
195
196
197def _show_files_reuse_message(file_paths):
198    """Shows the message of build failure but files existing and reusing them.
199
200    Args:
201        file_paths: A list of absolute file paths to be checked.
202    """
203    failed_or_file = ' or '.join(file_paths)
204    failed_and_file = ' and '.join(file_paths)
205    message = _GEN_JSON_FAILED.format(failed_or_file, failed_and_file)
206    print(_WARN_MSG.format(common_util.COLORED_INFO('Warning:'), message))
207
208
209def _show_build_failed_message(module_info, main_project=None):
210    """Show build failed message.
211
212    Args:
213        module_info: A ModuleInfo instance contains data of module-info.json.
214        main_project: A string of the main project name.
215    """
216    if main_project:
217        _, main_project_path = common_util.get_related_paths(
218            module_info, main_project)
219        _build_failed_handle(main_project_path)
220
221
222def _is_new_json_file_generated(json_path, original_file_mtime):
223    """Check the new file is generated or not.
224
225    Args:
226        json_path: The path of the json file being to check.
227        original_file_mtime: the original file modified time.
228
229    Returns:
230        A boolean, True if the json_path file is new generated, otherwise False.
231    """
232    if not os.path.isfile(json_path):
233        return False
234    return original_file_mtime != os.path.getmtime(json_path)
235
236
237def _build_failed_handle(main_project_path):
238    """Handle build failures.
239
240    Args:
241        main_project_path: The main project directory.
242
243    Handle results:
244        1) There's no project file, raise BuildFailureError.
245        2) There exists a project file, ask users if they want to
246           launch IDE with the old project file.
247           a) If the answer is yes, return.
248           b) If the answer is not yes, sys.exit(1)
249    """
250    project_file = glob.glob(
251        os.path.join(main_project_path, _INTELLIJ_PROJECT_FILE_EXT))
252    if project_file:
253        query = _LAUNCH_PROJECT_QUERY % project_file[0]
254        input_data = input(query)
255        if not input_data.lower() in ['yes', 'y']:
256            sys.exit(1)
257    else:
258        raise errors.BuildFailureError(
259            'Failed to generate %s.' % common_util.get_blueprint_json_path(
260                constant.BLUEPRINT_JAVA_JSONFILE_NAME))
261
262
263def _merge_module_keys(m_dict, b_dict):
264    """Merge a module's dictionary into another module's dictionary.
265
266    Merge b_dict module data into m_dict.
267
268    Args:
269        m_dict: The module dictionary is going to merge b_dict into.
270        b_dict: Soong build system module dictionary.
271    """
272    for key, b_modules in b_dict.items():
273        m_dict[key] = sorted(list(set(m_dict.get(key, []) + b_modules)))
274
275
276def _copy_needed_items_from(mk_dict):
277    """Shallow copy needed items from Make build system module info dictionary.
278
279    Args:
280        mk_dict: Make build system dictionary is going to be copied.
281
282    Returns:
283        A merged dictionary.
284    """
285    merged_dict = dict()
286    for module in mk_dict.keys():
287        merged_dict[module] = dict()
288        for key in mk_dict[module].keys():
289            if key in _MERGE_NEEDED_ITEMS and mk_dict[module][key] != []:
290                merged_dict[module][key] = mk_dict[module][key]
291    return merged_dict
292
293
294def _merge_dict(mk_dict, bp_dict):
295    """Merge two dictionaries.
296
297    Linked function:
298        _merge_module_keys(m_dict, b_dict)
299
300    Args:
301        mk_dict: Make build system module info dictionary.
302        bp_dict: Soong build system module info dictionary.
303
304    Returns:
305        A merged dictionary.
306    """
307    merged_dict = _copy_needed_items_from(mk_dict)
308    for module in bp_dict.keys():
309        if module not in merged_dict.keys():
310            merged_dict[module] = dict()
311        _merge_module_keys(merged_dict[module], bp_dict[module])
312    return merged_dict
313