#!/usr/bin/env python3 # # Copyright 2018 - The Android Open Source Project # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. """module_info_util This module receives a module path which is relative to its root directory and makes a command to generate two json files, one for mk files and one for bp files. Then it will load these two json files into two json dictionaries, merge them into one dictionary and return the merged dictionary to its caller. Example usage: merged_dict = generate_merged_module_info() """ import glob import logging import os import sys from aidegen import constant from aidegen.lib import common_util from aidegen.lib import errors from aidegen.lib import project_config from atest import atest_utils _MERGE_NEEDED_ITEMS = [ constant.KEY_CLASS, constant.KEY_PATH, constant.KEY_INSTALLED, constant.KEY_DEPENDENCIES, constant.KEY_SRCS, constant.KEY_SRCJARS, constant.KEY_CLASSES_JAR, constant.KEY_TAG, constant.KEY_COMPATIBILITY, constant.KEY_AUTO_TEST_CONFIG, constant.KEY_MODULE_NAME, constant.KEY_TEST_CONFIG ] _INTELLIJ_PROJECT_FILE_EXT = '*.iml' _LAUNCH_PROJECT_QUERY = ( 'There exists an IntelliJ project file: %s. Do you want ' 'to launch it (yes/No)?') _BUILD_BP_JSON_ENV_ON = { constant.GEN_JAVA_DEPS: 'true', constant.GEN_CC_DEPS: 'true', constant.GEN_COMPDB: 'true', constant.GEN_RUST: 'true' } _GEN_JSON_FAILED = ( 'Generate new {0} failed, AIDEGen will proceed and reuse the old {1}.') _TARGET = 'nothing' _LINKFILE_WARNING = ( 'File {} does not exist and we can not make a symbolic link for it.') _RUST_PROJECT_JSON = 'out/soong/rust-project.json' # pylint: disable=dangerous-default-value @common_util.back_to_cwd @common_util.time_logged def generate_merged_module_info(env_on=_BUILD_BP_JSON_ENV_ON): """Generate a merged dictionary. Linked functions: _build_bp_info(module_info, project, verbose, skip_build) _get_soong_build_json_dict() _merge_dict(mk_dict, bp_dict) Args: env_on: A dictionary of environment settings to be turned on, the default value is _BUILD_BP_JSON_ENV_ON. Returns: A merged dictionary from module-info.json and module_bp_java_deps.json. """ config = project_config.ProjectConfig.get_instance() module_info = config.atest_module_info projects = config.targets verbose = True skip_build = config.is_skip_build main_project = projects[0] if projects else None _build_bp_info( module_info, main_project, verbose, skip_build, env_on) json_path = common_util.get_blueprint_json_path( constant.BLUEPRINT_JAVA_JSONFILE_NAME) bp_dict = common_util.get_json_dict(json_path) return _merge_dict(module_info.name_to_module_info, bp_dict) def _build_bp_info(module_info, main_project=None, verbose=False, skip_build=False, env_on=_BUILD_BP_JSON_ENV_ON): """Make nothing to create module_bp_java_deps.json, module_bp_cc_deps.json. Use atest build method to build the target 'nothing' by setting env config SOONG_COLLECT_JAVA_DEPS to true to trigger the process of collecting dependencies and generate module_bp_java_deps.json etc. Args: module_info: A ModuleInfo instance contains data of module-info.json. main_project: A string of the main project name. verbose: A boolean, if true displays full build output. skip_build: A boolean, if true, skip building if get_blueprint_json_path(file_name) file exists, otherwise build it. env_on: A dictionary of environment settings to be turned on, the default value is _BUILD_BP_JSON_ENV_ON. Build results: 1. Build successfully return. 2. Build failed: 1) There's no project file, raise BuildFailureError. 2) There exists a project file, ask users if they want to launch IDE with the old project file. a) If the answer is yes, return. b) If the answer is not yes, sys.exit(1) """ file_paths = _get_generated_json_files(env_on) files_exist = all([os.path.isfile(fpath) for fpath in file_paths]) files = '\n'.join(file_paths) if skip_build and files_exist: logging.info('Files:\n%s exist, skipping build.', files) return original_file_mtimes = {f: None for f in file_paths} if files_exist: original_file_mtimes = {f: os.path.getmtime(f) for f in file_paths} logging.warning( '\nGenerate files:\n %s by atest build method.', files) build_with_on_cmd = atest_utils.build([_TARGET], verbose, env_on) # For Android Rust projects, we need to create a symbolic link to the file # out/soong/rust-project.json to launch the rust projects in IDEs. _generate_rust_project_link() if build_with_on_cmd: logging.info('\nGenerate blueprint json successfully.') else: if not all([_is_new_json_file_generated( f, original_file_mtimes[f]) for f in file_paths]): if files_exist: _show_files_reuse_message(file_paths) else: _show_build_failed_message(module_info, main_project) def _get_generated_json_files(env_on=_BUILD_BP_JSON_ENV_ON): """Gets the absolute paths of the files which is going to be generated. Determine the files which will be generated by the environment on dictionary and the default blueprint json files' dictionary. The generation of json files depends on env_on. If the env_on looks like, _BUILD_BP_JSON_ENV_ON = { 'SOONG_COLLECT_JAVA_DEPS': 'true', 'SOONG_COLLECT_CC_DEPS': 'true', 'SOONG_GEN_COMPDB': 'true', 'SOONG_GEN_RUST_PROJECT': 'true' } We want to generate 4 files: module_bp_java_deps.json, module_bp_cc_deps.json, compile_commands.json and rust-project.json. And in get_blueprint_json_files_relative_dict function, there are 4 json files by default and return a result list of the absolute paths of the existent files. Args: env_on: A dictionary of environment settings to be turned on, the default value is _BUILD_BP_JSON_ENV_ON. Returns: A list of the absolute paths of the files which is going to be generated. """ json_files_dict = common_util.get_blueprint_json_files_relative_dict() file_paths = [] for key in env_on: if not env_on[key] == 'true' or key not in json_files_dict: continue file_paths.append(json_files_dict[key]) return file_paths def _show_files_reuse_message(file_paths): """Shows the message of build failure but files existing and reusing them. Args: file_paths: A list of absolute file paths to be checked. """ failed_or_file = ' or '.join(file_paths) failed_and_file = ' and '.join(file_paths) message = _GEN_JSON_FAILED.format(failed_or_file, failed_and_file) print(constant.WARN_MSG.format( common_util.COLORED_INFO('Warning:'), message)) def _show_build_failed_message(module_info, main_project=None): """Show build failed message. Args: module_info: A ModuleInfo instance contains data of module-info.json. main_project: A string of the main project name. """ if main_project: _, main_project_path = common_util.get_related_paths( module_info, main_project) _build_failed_handle(main_project_path) def _is_new_json_file_generated(json_path, original_file_mtime): """Check the new file is generated or not. Args: json_path: The path of the json file being to check. original_file_mtime: the original file modified time. Returns: A boolean, True if the json_path file is new generated, otherwise False. """ if not os.path.isfile(json_path): return False return original_file_mtime != os.path.getmtime(json_path) def _build_failed_handle(main_project_path): """Handle build failures. Args: main_project_path: The main project directory. Handle results: 1) There's no project file, raise BuildFailureError. 2) There exists a project file, ask users if they want to launch IDE with the old project file. a) If the answer is yes, return. b) If the answer is not yes, sys.exit(1) """ project_file = glob.glob( os.path.join(main_project_path, _INTELLIJ_PROJECT_FILE_EXT)) if project_file: query = _LAUNCH_PROJECT_QUERY % project_file[0] input_data = input(query) if not input_data.lower() in ['yes', 'y']: sys.exit(1) else: raise errors.BuildFailureError( 'Failed to generate %s.' % common_util.get_blueprint_json_path( constant.BLUEPRINT_JAVA_JSONFILE_NAME)) def _merge_module_keys(m_dict, b_dict): """Merge a module's dictionary into another module's dictionary. Merge b_dict module data into m_dict. Args: m_dict: The module dictionary is going to merge b_dict into. b_dict: Soong build system module dictionary. """ for key, b_modules in b_dict.items(): m_dict[key] = sorted(list(set(m_dict.get(key, []) + b_modules))) def _copy_needed_items_from(mk_dict): """Shallow copy needed items from Make build system module info dictionary. Args: mk_dict: Make build system dictionary is going to be copied. Returns: A merged dictionary. """ merged_dict = dict() for module in mk_dict.keys(): merged_dict[module] = dict() for key in mk_dict[module].keys(): if key in _MERGE_NEEDED_ITEMS and mk_dict[module][key] != []: merged_dict[module][key] = mk_dict[module][key] return merged_dict def _merge_dict(mk_dict, bp_dict): """Merge two dictionaries. Linked function: _merge_module_keys(m_dict, b_dict) Args: mk_dict: Make build system module info dictionary. bp_dict: Soong build system module info dictionary. Returns: A merged dictionary. """ merged_dict = _copy_needed_items_from(mk_dict) for module in bp_dict.keys(): if module not in merged_dict.keys(): merged_dict[module] = dict() _merge_module_keys(merged_dict[module], bp_dict[module]) return merged_dict def _generate_rust_project_link(): """Generates out/soong/rust-project.json symbolic link in Android root.""" root_dir = common_util.get_android_root_dir() rust_project = os.path.join( root_dir, common_util.get_blueprint_json_path( constant.RUST_PROJECT_JSON)) if not os.path.isfile(rust_project): message = _LINKFILE_WARNING.format(_RUST_PROJECT_JSON) print(constant.WARN_MSG.format( common_util.COLORED_INFO('Warning:'), message)) return link_rust = os.path.join(root_dir, constant.RUST_PROJECT_JSON) if os.path.islink(link_rust): os.remove(link_rust) os.symlink(rust_project, link_rust)