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