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_ON = {
59    constant.GEN_JAVA_DEPS: 'true',
60    constant.GEN_CC_DEPS: 'true',
61    constant.GEN_COMPDB: 'true',
62    constant.GEN_RUST: 'true'
63}
64_GEN_JSON_FAILED = (
65    'Generate new {0} failed, AIDEGen will proceed and reuse the old {1}.')
66_WARN_MSG = '\n{} {}\n'
67_TARGET = 'nothing'
68_LINKFILE_WARNING = (
69    'File {} does not exist and we can not make a symbolic link for it.')
70_RUST_PROJECT_JSON = 'out/soong/rust-project.json'
71
72
73# pylint: disable=dangerous-default-value
74@common_util.back_to_cwd
75@common_util.time_logged
76def generate_merged_module_info(env_on=_BUILD_BP_JSON_ENV_ON):
77    """Generate a merged dictionary.
78
79    Linked functions:
80        _build_bp_info(module_info, project, verbose, skip_build)
81        _get_soong_build_json_dict()
82        _merge_dict(mk_dict, bp_dict)
83
84    Args:
85        env_on: A dictionary of environment settings to be turned on, the
86                default value is _BUILD_BP_JSON_ENV_ON.
87
88    Returns:
89        A merged dictionary from module-info.json and module_bp_java_deps.json.
90    """
91    config = project_config.ProjectConfig.get_instance()
92    module_info = config.atest_module_info
93    projects = config.targets
94    verbose = True
95    skip_build = config.is_skip_build
96    main_project = projects[0] if projects else None
97    _build_bp_info(
98        module_info, main_project, verbose, skip_build, env_on)
99    json_path = common_util.get_blueprint_json_path(
100        constant.BLUEPRINT_JAVA_JSONFILE_NAME)
101    bp_dict = common_util.get_json_dict(json_path)
102    return _merge_dict(module_info.name_to_module_info, bp_dict)
103
104
105def _build_bp_info(module_info, main_project=None, verbose=False,
106                   skip_build=False, env_on=_BUILD_BP_JSON_ENV_ON):
107    """Make nothing to create module_bp_java_deps.json, module_bp_cc_deps.json.
108
109    Use atest build method to build the target 'nothing' by setting env config
110    SOONG_COLLECT_JAVA_DEPS to true to trigger the process of collecting
111    dependencies and generate module_bp_java_deps.json etc.
112
113    Args:
114        module_info: A ModuleInfo instance contains data of module-info.json.
115        main_project: A string of the main project name.
116        verbose: A boolean, if true displays full build output.
117        skip_build: A boolean, if true, skip building if
118                    get_blueprint_json_path(file_name) file exists, otherwise
119                    build it.
120        env_on: A dictionary of environment settings to be turned on, the
121                default value is _BUILD_BP_JSON_ENV_ON.
122
123    Build results:
124        1. Build successfully return.
125        2. Build failed:
126           1) There's no project file, raise BuildFailureError.
127           2) There exists a project file, ask users if they want to
128              launch IDE with the old project file.
129              a) If the answer is yes, return.
130              b) If the answer is not yes, sys.exit(1)
131    """
132    file_paths = _get_generated_json_files(env_on)
133    files_exist = all([os.path.isfile(fpath) for fpath in file_paths])
134    files = '\n'.join(file_paths)
135    if skip_build and files_exist:
136        logging.info('Files:\n%s exist, skipping build.', files)
137        return
138    original_file_mtimes = {f: None for f in file_paths}
139    if files_exist:
140        original_file_mtimes = {f: os.path.getmtime(f) for f in file_paths}
141
142    logging.warning(
143        '\nGenerate files:\n %s by atest build method.', files)
144    build_with_on_cmd = atest_utils.build([_TARGET], verbose, env_on)
145
146    # For Android Rust projects, we need to create a symbolic link to the file
147    # out/soong/rust-project.json to launch the rust projects in IDEs.
148    _generate_rust_project_link()
149
150    if build_with_on_cmd:
151        logging.info('\nGenerate blueprint json successfully.')
152    else:
153        if not all([_is_new_json_file_generated(
154                f, original_file_mtimes[f]) for f in file_paths]):
155            if files_exist:
156                _show_files_reuse_message(file_paths)
157            else:
158                _show_build_failed_message(module_info, main_project)
159
160
161def _get_generated_json_files(env_on=_BUILD_BP_JSON_ENV_ON):
162    """Gets the absolute paths of the files which is going to be generated.
163
164    Determine the files which will be generated by the environment on dictionary
165    and the default blueprint json files' dictionary.
166    The generation of json files depends on env_on. If the env_on looks like,
167    _BUILD_BP_JSON_ENV_ON = {
168        'SOONG_COLLECT_JAVA_DEPS': 'true',
169        'SOONG_COLLECT_CC_DEPS': 'true',
170        'SOONG_GEN_COMPDB': 'true',
171        'SOONG_GEN_RUST_PROJECT': 'true'
172    }
173    We want to generate 4 files: module_bp_java_deps.json,
174    module_bp_cc_deps.json, compile_commands.json and rust-project.json. And in
175    get_blueprint_json_files_relative_dict function, there are 4 json files
176    by default and return a result list of the absolute paths of the existent
177    files.
178
179    Args:
180        env_on: A dictionary of environment settings to be turned on, the
181                default value is _BUILD_BP_JSON_ENV_ON.
182
183    Returns:
184        A list of the absolute paths of the files which is going to be
185        generated.
186    """
187    json_files_dict = common_util.get_blueprint_json_files_relative_dict()
188    file_paths = []
189    for key in env_on:
190        if not env_on[key] == 'true' or key not in json_files_dict:
191            continue
192        file_paths.append(json_files_dict[key])
193    return file_paths
194
195
196def _show_files_reuse_message(file_paths):
197    """Shows the message of build failure but files existing and reusing them.
198
199    Args:
200        file_paths: A list of absolute file paths to be checked.
201    """
202    failed_or_file = ' or '.join(file_paths)
203    failed_and_file = ' and '.join(file_paths)
204    message = _GEN_JSON_FAILED.format(failed_or_file, failed_and_file)
205    print(_WARN_MSG.format(common_util.COLORED_INFO('Warning:'), message))
206
207
208def _show_build_failed_message(module_info, main_project=None):
209    """Show build failed message.
210
211    Args:
212        module_info: A ModuleInfo instance contains data of module-info.json.
213        main_project: A string of the main project name.
214    """
215    if main_project:
216        _, main_project_path = common_util.get_related_paths(
217            module_info, main_project)
218        _build_failed_handle(main_project_path)
219
220
221def _is_new_json_file_generated(json_path, original_file_mtime):
222    """Check the new file is generated or not.
223
224    Args:
225        json_path: The path of the json file being to check.
226        original_file_mtime: the original file modified time.
227
228    Returns:
229        A boolean, True if the json_path file is new generated, otherwise False.
230    """
231    if not os.path.isfile(json_path):
232        return False
233    return original_file_mtime != os.path.getmtime(json_path)
234
235
236def _build_failed_handle(main_project_path):
237    """Handle build failures.
238
239    Args:
240        main_project_path: The main project directory.
241
242    Handle results:
243        1) There's no project file, raise BuildFailureError.
244        2) There exists a project file, ask users if they want to
245           launch IDE with the old project file.
246           a) If the answer is yes, return.
247           b) If the answer is not yes, sys.exit(1)
248    """
249    project_file = glob.glob(
250        os.path.join(main_project_path, _INTELLIJ_PROJECT_FILE_EXT))
251    if project_file:
252        query = _LAUNCH_PROJECT_QUERY % project_file[0]
253        input_data = input(query)
254        if not input_data.lower() in ['yes', 'y']:
255            sys.exit(1)
256    else:
257        raise errors.BuildFailureError(
258            'Failed to generate %s.' % common_util.get_blueprint_json_path(
259                constant.BLUEPRINT_JAVA_JSONFILE_NAME))
260
261
262def _merge_module_keys(m_dict, b_dict):
263    """Merge a module's dictionary into another module's dictionary.
264
265    Merge b_dict module data into m_dict.
266
267    Args:
268        m_dict: The module dictionary is going to merge b_dict into.
269        b_dict: Soong build system module dictionary.
270    """
271    for key, b_modules in b_dict.items():
272        m_dict[key] = sorted(list(set(m_dict.get(key, []) + b_modules)))
273
274
275def _copy_needed_items_from(mk_dict):
276    """Shallow copy needed items from Make build system module info dictionary.
277
278    Args:
279        mk_dict: Make build system dictionary is going to be copied.
280
281    Returns:
282        A merged dictionary.
283    """
284    merged_dict = dict()
285    for module in mk_dict.keys():
286        merged_dict[module] = dict()
287        for key in mk_dict[module].keys():
288            if key in _MERGE_NEEDED_ITEMS and mk_dict[module][key] != []:
289                merged_dict[module][key] = mk_dict[module][key]
290    return merged_dict
291
292
293def _merge_dict(mk_dict, bp_dict):
294    """Merge two dictionaries.
295
296    Linked function:
297        _merge_module_keys(m_dict, b_dict)
298
299    Args:
300        mk_dict: Make build system module info dictionary.
301        bp_dict: Soong build system module info dictionary.
302
303    Returns:
304        A merged dictionary.
305    """
306    merged_dict = _copy_needed_items_from(mk_dict)
307    for module in bp_dict.keys():
308        if module not in merged_dict.keys():
309            merged_dict[module] = dict()
310        _merge_module_keys(merged_dict[module], bp_dict[module])
311    return merged_dict
312
313
314def _generate_rust_project_link():
315    """Generates out/soong/rust-project.json symbolic link in Android root."""
316    root_dir = common_util.get_android_root_dir()
317    rust_project = os.path.join(
318        root_dir, common_util.get_blueprint_json_path(
319            constant.RUST_PROJECT_JSON))
320    if not os.path.isfile(rust_project):
321        message = _LINKFILE_WARNING.format(_RUST_PROJECT_JSON)
322        print(_WARN_MSG.format(common_util.COLORED_INFO('Warning:'), message))
323        return
324    link_rust = os.path.join(root_dir, constant.RUST_PROJECT_JSON)
325    if os.path.islink(link_rust):
326        os.remove(link_rust)
327    os.symlink(rust_project, link_rust)
328