#!/usr/bin/env python3 # # Copyright 2019 - 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. """It is an AIDEGen sub task: generate the CLion project file. Usage example: json_path = common_util.get_blueprint_json_path( constant.BLUEPRINT_CC_JSONFILE_NAME) json_dict = common_util.get_soong_build_json_dict(json_path) if 'modules' not in json_dict: return mod_info = json_dict['modules'].get('libui', {}) if not mod_info: return CLionProjectFileGenerator(mod_info).generate_cmakelists_file() """ import logging import os from io import StringIO from io import TextIOWrapper from aidegen import constant from aidegen import templates from aidegen.lib import common_util from aidegen.lib import errors from aidegen.lib import native_module_info # Flags for writing to CMakeLists.txt section. _GLOBAL_COMMON_FLAGS = '\n# GLOBAL ALL FLAGS:\n' _LOCAL_COMMON_FLAGS = '\n# LOCAL ALL FLAGS:\n' _GLOBAL_CFLAGS = '\n# GLOBAL CFLAGS:\n' _LOCAL_CFLAGS = '\n# LOCAL CFLAGS:\n' _GLOBAL_C_ONLY_FLAGS = '\n# GLOBAL C ONLY FLAGS:\n' _LOCAL_C_ONLY_FLAGS = '\n# LOCAL C ONLY FLAGS:\n' _GLOBAL_CPP_FLAGS = '\n# GLOBAL CPP FLAGS:\n' _LOCAL_CPP_FLAGS = '\n# LOCAL CPP FLAGS:\n' _SYSTEM_INCLUDE_FLAGS = '\n# GLOBAL SYSTEM INCLUDE FLAGS:\n' # Keys for writing in module_bp_cc_deps.json _KEY_GLOBAL_COMMON_FLAGS = 'global_common_flags' _KEY_LOCAL_COMMON_FLAGS = 'local_common_flags' _KEY_GLOBAL_CFLAGS = 'global_c_flags' _KEY_LOCAL_CFLAGS = 'local_c_flags' _KEY_GLOBAL_C_ONLY_FLAGS = 'global_c_only_flags' _KEY_LOCAL_C_ONLY_FLAGS = 'local_c_only_flags' _KEY_GLOBAL_CPP_FLAGS = 'global_cpp_flags' _KEY_LOCAL_CPP_FLAGS = 'local_cpp_flags' _KEY_SYSTEM_INCLUDE_FLAGS = 'system_include_flags' # Dictionary maps keys to sections. _FLAGS_DICT = { _KEY_GLOBAL_COMMON_FLAGS: _GLOBAL_COMMON_FLAGS, _KEY_LOCAL_COMMON_FLAGS: _LOCAL_COMMON_FLAGS, _KEY_GLOBAL_CFLAGS: _GLOBAL_CFLAGS, _KEY_LOCAL_CFLAGS: _LOCAL_CFLAGS, _KEY_GLOBAL_C_ONLY_FLAGS: _GLOBAL_C_ONLY_FLAGS, _KEY_LOCAL_C_ONLY_FLAGS: _LOCAL_C_ONLY_FLAGS, _KEY_GLOBAL_CPP_FLAGS: _GLOBAL_CPP_FLAGS, _KEY_LOCAL_CPP_FLAGS: _LOCAL_CPP_FLAGS, _KEY_SYSTEM_INCLUDE_FLAGS: _SYSTEM_INCLUDE_FLAGS } # Keys for parameter types. _KEY_FLAG = 'flag' _KEY_SYSTEM_ROOT = 'system_root' _KEY_RELATIVE = 'relative_file_path' # Constants for CMakeLists.txt. _MIN_VERSION_TOKEN = '@MINVERSION@' _PROJECT_NAME_TOKEN = '@PROJNAME@' _ANDOIR_ROOT_TOKEN = '@ANDROIDROOT@' _MINI_VERSION_SUPPORT = 'cmake_minimum_required(VERSION {})\n' _MINI_VERSION = '3.5' _KEY_CLANG = 'clang' _KEY_CPPLANG = 'clang++' _SET_C_COMPILER = 'set(CMAKE_C_COMPILER \"{}\")\n' _SET_CXX_COMPILER = 'set(CMAKE_CXX_COMPILER \"{}\")\n' _LIST_APPEND_HEADER = 'list(APPEND\n' _SOURCE_FILES_HEADER = 'SOURCE_FILES' _SOURCE_FILES_LINE = ' SOURCE_FILES\n' _END_WITH_ONE_BLANK_LINE = ')\n' _END_WITH_TWO_BLANK_LINES = ')\n\n' _SET_RELATIVE_PATH = 'set({} "{} {}={}")\n' _SET_ALL_FLAGS = 'set({} "{} {}")\n' _ANDROID_ROOT_SYMBOL = '${ANDROID_ROOT}' _SYSTEM = 'SYSTEM' _INCLUDE_DIR = 'include_directories({} \n' _SET_INCLUDE_FORMAT = ' "{}"\n' _CMAKE_C_FLAGS = 'CMAKE_C_FLAGS' _CMAKE_CXX_FLAGS = 'CMAKE_CXX_FLAGS' _USR = 'usr' _INCLUDE = 'include' _INCLUDE_SYSTEM = 'include_directories(SYSTEM "{}")\n' _GLOB_RECURSE_TMP_HEADERS = 'file (GLOB_RECURSE TMP_HEADERS\n' _ALL_HEADER_FILES = ' "{}/**/*.h"\n' _APPEND_SOURCE_FILES = "list (APPEND SOURCE_FILES ${TMP_HEADERS})\n\n" _ADD_EXECUTABLE_HEADER = '\nadd_executable({} {})' _PROJECT = 'project({})\n' _ADD_SUB = 'add_subdirectory({})\n' _DICT_EMPTY = 'mod_info is empty.' _DICT_NO_MOD_NAME_KEY = "mod_info does not contain 'module_name' key." _DICT_NO_PATH_KEY = "mod_info does not contain 'path' key." _MODULE_INFO_EMPTY = 'The module info dictionary is empty.' class CLionProjectFileGenerator: """CLion project file generator. Attributes: mod_info: A dictionary of the target module's info. mod_name: A string of module name. mod_path: A string of module's path. cc_dir: A string of generated CLion project file's directory. cc_path: A string of generated CLion project file's path. """ def __init__(self, mod_info, parent_dir=None): """ProjectFileGenerator initialize. Args: mod_info: A dictionary of native module's info. parent_dir: The parent directory of this native module. The default value is None. """ if not mod_info: raise errors.ModuleInfoEmptyError(_MODULE_INFO_EMPTY) self.mod_info = mod_info self.mod_name = self._get_module_name() self.mod_path = CLionProjectFileGenerator.get_module_path( mod_info, parent_dir) self.cc_dir = CLionProjectFileGenerator.get_cmakelists_file_dir( os.path.join(self.mod_path, self.mod_name)) if not os.path.exists(self.cc_dir): os.makedirs(self.cc_dir) self.cc_path = os.path.join(self.cc_dir, constant.CLION_PROJECT_FILE_NAME) def _get_module_name(self): """Gets the value of the 'module_name' key if it exists. Returns: A string of the module's name. Raises: NoModuleNameDefinedInModuleInfoError if no 'module_name' key in mod_info. """ mod_name = self.mod_info.get(constant.KEY_MODULE_NAME, '') if not mod_name: raise errors.NoModuleNameDefinedInModuleInfoError( _DICT_NO_MOD_NAME_KEY) return mod_name @staticmethod def get_module_path(mod_info, parent_dir=None): """Gets the correct value of the 'path' key if it exists. When a module with different paths, e.g., 'libqcomvoiceprocessingdescriptors': { 'path': [ 'device/google/bonito/voice_processing', 'device/google/coral/voice_processing', 'device/google/crosshatch/voice_processing', 'device/google/muskie/voice_processing', 'device/google/taimen/voice_processing' ], ... } it might be wrong if we always choose the first path. For example, in this case if users command 'aidegen -i c device/google/coral' the correct path they need should be the second one. Args: mod_info: A module's info dictionary. parent_dir: The parent directory of this native module. The default value is None. Returns: A string of the module's path. Raises: NoPathDefinedInModuleInfoError if no 'path' key in mod_info. """ mod_paths = mod_info.get(constant.KEY_PATH, []) if not mod_paths: raise errors.NoPathDefinedInModuleInfoError(_DICT_NO_PATH_KEY) mod_path = mod_paths[0] if parent_dir and len(mod_paths) > 1: for path in mod_paths: if common_util.is_source_under_relative_path(path, parent_dir): mod_path = path return mod_path @staticmethod @common_util.check_args(cc_path=str) def get_cmakelists_file_dir(cc_path): """Gets module's CMakeLists.txt file path to be created. Return a string of $OUT/development/ide/clion/${cc_path}. For example, if module name is 'libui'. The return path string would be: out/development/ide/clion/frameworks/native/libs/ui/libui Args: cc_path: A string of absolute path of module's Android.bp file. Returns: A string of absolute path of module's CMakeLists.txt file to be created. """ return os.path.join(common_util.get_android_root_dir(), common_util.get_android_out_dir(), constant.RELATIVE_NATIVE_PATH, cc_path) def generate_cmakelists_file(self): """Generates CLion project file from the target module's info.""" with open(self.cc_path, 'w') as hfile: self._write_cmakelists_file(hfile) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_cmakelists_file(self, hfile): """Writes CLion project file content with neccessary info. Args: hfile: A file handler instance. """ self._write_header(hfile) self._write_c_compiler_paths(hfile) self._write_source_files(hfile) self._write_cmakelists_flags(hfile) self._write_tail(hfile) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_header(self, hfile): """Writes CLion project file's header messages. Args: hfile: A file handler instance. """ content = templates.CMAKELISTS_HEADER.replace( _MIN_VERSION_TOKEN, _MINI_VERSION) content = content.replace(_PROJECT_NAME_TOKEN, self.mod_name) content = content.replace( _ANDOIR_ROOT_TOKEN, common_util.get_android_root_dir()) hfile.write(content) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_c_compiler_paths(self, hfile): """Writes CMake compiler paths for C and Cpp to CLion project file. Args: hfile: A file handler instance. """ hfile.write(_SET_C_COMPILER.format( native_module_info.NativeModuleInfo.c_lang_path)) hfile.write(_SET_CXX_COMPILER.format( native_module_info.NativeModuleInfo.cpp_lang_path)) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_source_files(self, hfile): """Writes source files' paths to CLion project file. Args: hfile: A file handler instance. """ if constant.KEY_SRCS not in self.mod_info: logging.warning("No source files in %s's module info.", self.mod_name) return root = common_util.get_android_root_dir() source_files = self.mod_info[constant.KEY_SRCS] hfile.write(_LIST_APPEND_HEADER) hfile.write(_SOURCE_FILES_LINE) for src in source_files: if not os.path.exists(os.path.join(root, src)): continue hfile.write(''.join([_build_cmake_path(src, ' '), '\n'])) hfile.write(_END_WITH_ONE_BLANK_LINE) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_cmakelists_flags(self, hfile): """Writes all kinds of flags in CLion project file. Args: hfile: A file handler instance. """ self._write_flags(hfile, _KEY_GLOBAL_COMMON_FLAGS, True, True) self._write_flags(hfile, _KEY_LOCAL_COMMON_FLAGS, True, True) self._write_flags(hfile, _KEY_GLOBAL_CFLAGS, True, True) self._write_flags(hfile, _KEY_LOCAL_CFLAGS, True, True) self._write_flags(hfile, _KEY_GLOBAL_C_ONLY_FLAGS, True, False) self._write_flags(hfile, _KEY_LOCAL_C_ONLY_FLAGS, True, False) self._write_flags(hfile, _KEY_GLOBAL_CPP_FLAGS, False, True) self._write_flags(hfile, _KEY_LOCAL_CPP_FLAGS, False, True) self._write_flags(hfile, _KEY_SYSTEM_INCLUDE_FLAGS, True, True) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_tail(self, hfile): """Writes CLion project file content with necessary info. Args: hfile: A file handler instance. """ hfile.write( _ADD_EXECUTABLE_HEADER.format( _cleanup_executable_name(self.mod_name), _add_dollar_sign(_SOURCE_FILES_HEADER))) @common_util.check_args( hfile=(TextIOWrapper, StringIO), key=str, cflags=bool, cppflags=bool) @common_util.io_error_handle def _write_flags(self, hfile, key, cflags, cppflags): """Writes CMake compiler paths of C, Cpp for different kinds of flags. Args: hfile: A file handler instance. key: A string of flag type, e.g., 'global_common_flags' flag. cflags: A boolean for setting 'CMAKE_C_FLAGS' flag. cppflags: A boolean for setting 'CMAKE_CXX_FLAGS' flag. """ if key not in _FLAGS_DICT: return hfile.write(_FLAGS_DICT[key]) params_dict = self._parse_compiler_parameters(key) if params_dict: _translate_to_cmake(hfile, params_dict, cflags, cppflags) @common_util.check_args(flag=str) def _parse_compiler_parameters(self, flag): """Parses the specific flag data from a module_info dictionary. Args: flag: The string of key flag, e.g.: _KEY_GLOBAL_COMMON_FLAGS. Returns: A dictionary with compiled parameters. """ params = self.mod_info.get(flag, {}) if not params: return None params_dict = { constant.KEY_HEADER: [], constant.KEY_SYSTEM: [], _KEY_FLAG: [], _KEY_SYSTEM_ROOT: '', _KEY_RELATIVE: {} } for key, value in params.items(): params_dict[key] = value return params_dict @common_util.check_args(rel_project_path=str, mod_names=list) @common_util.io_error_handle def generate_base_cmakelists_file(cc_module_info, rel_project_path, mod_names): """Generates base CLion project file for multiple CLion projects. We create a multiple native project file: {android_root}/development/ide/clion/{rel_project_path}/CMakeLists.txt and use this project file to generate a link: {android_root}/out/development/ide/clion/{rel_project_path}/CMakeLists.txt Args: cc_module_info: An instance of native_module_info.NativeModuleInfo. rel_project_path: A string of the base project relative path. For example: frameworks/native/libs/ui. mod_names: A list of module names whose project were created under rel_project_path. Returns: A symbolic link CLion project file path. """ root_dir = common_util.get_android_root_dir() cc_dir = os.path.join(root_dir, constant.RELATIVE_NATIVE_PATH, rel_project_path) cc_out_dir = os.path.join(root_dir, common_util.get_android_out_dir(), constant.RELATIVE_NATIVE_PATH, rel_project_path) if not os.path.exists(cc_dir): os.makedirs(cc_dir) dst_path = os.path.join(cc_out_dir, constant.CLION_PROJECT_FILE_NAME) if os.path.islink(dst_path): os.unlink(dst_path) src_path = os.path.join(cc_dir, constant.CLION_PROJECT_FILE_NAME) if os.path.isfile(src_path): os.remove(src_path) with open(src_path, 'w') as hfile: _write_base_cmakelists_file(hfile, cc_module_info, src_path, mod_names) os.symlink(src_path, dst_path) return dst_path @common_util.check_args( hfile=(TextIOWrapper, StringIO), abs_project_path=str, mod_names=list) @common_util.io_error_handle def _write_base_cmakelists_file(hfile, cc_module_info, abs_project_path, mod_names): """Writes base CLion project file content. When we write module info into base CLion project file, first check if the module's CMakeLists.txt exists. If file exists, write content, add_subdirectory({'relative_module_path'}) Args: hfile: A file handler instance. cc_module_info: An instance of native_module_info.NativeModuleInfo. abs_project_path: A string of the base project absolute path. For example, ${ANDROID_BUILD_TOP}/frameworks/native/libs/ui. mod_names: A list of module names whose project were created under abs_project_path. """ hfile.write(_MINI_VERSION_SUPPORT.format(_MINI_VERSION)) project_dir = os.path.dirname(abs_project_path) hfile.write(_PROJECT.format(os.path.basename(project_dir))) root_dir = common_util.get_android_root_dir() parent_dir = os.path.relpath(abs_project_path, root_dir) for mod_name in mod_names: mod_info = cc_module_info.get_module_info(mod_name) mod_path = CLionProjectFileGenerator.get_module_path( mod_info, parent_dir) file_dir = CLionProjectFileGenerator.get_cmakelists_file_dir( os.path.join(mod_path, mod_name)) file_path = os.path.join(file_dir, constant.CLION_PROJECT_FILE_NAME) if not os.path.isfile(file_path): logging.warning("%s the project file %s doesn't exist.", common_util.COLORED_INFO('Warning:'), file_path) continue link_project_dir = os.path.join(root_dir, common_util.get_android_out_dir(), os.path.relpath(project_dir, root_dir)) rel_mod_path = os.path.relpath(file_dir, link_project_dir) hfile.write(_ADD_SUB.format(rel_mod_path)) @common_util.check_args( hfile=(TextIOWrapper, StringIO), params_dict=dict, cflags=bool, cppflags=bool) def _translate_to_cmake(hfile, params_dict, cflags, cppflags): """Translates parameter dict's contents into CLion project file format. Args: hfile: A file handler instance. params_dict: A dict contains data to be translated into CLion project file format. cflags: A boolean is to set 'CMAKE_C_FLAGS' flag. cppflags: A boolean is to set 'CMAKE_CXX_FLAGS' flag. """ _write_all_include_directories( hfile, params_dict[constant.KEY_SYSTEM], True) _write_all_include_directories( hfile, params_dict[constant.KEY_HEADER], False) if cflags: _write_all_relative_file_path_flags(hfile, params_dict[_KEY_RELATIVE], _CMAKE_C_FLAGS) _write_all_flags(hfile, params_dict[_KEY_FLAG], _CMAKE_C_FLAGS) if cppflags: _write_all_relative_file_path_flags(hfile, params_dict[_KEY_RELATIVE], _CMAKE_CXX_FLAGS) _write_all_flags(hfile, params_dict[_KEY_FLAG], _CMAKE_CXX_FLAGS) if params_dict[_KEY_SYSTEM_ROOT]: path = os.path.join(params_dict[_KEY_SYSTEM_ROOT], _USR, _INCLUDE) hfile.write(_INCLUDE_SYSTEM.format(_build_cmake_path(path))) @common_util.check_args(hfile=(TextIOWrapper, StringIO), is_system=bool) @common_util.io_error_handle def _write_all_include_directories(hfile, includes, is_system): """Writes all included directories' paths to the CLion project file. Args: hfile: A file handler instance. includes: A list of included file paths. is_system: A boolean of whether it's a system flag. """ if not includes: return _write_all_includes(hfile, includes, is_system) _write_all_headers(hfile, includes) @common_util.check_args( hfile=(TextIOWrapper, StringIO), rel_paths_dict=dict, tag=str) @common_util.io_error_handle def _write_all_relative_file_path_flags(hfile, rel_paths_dict, tag): """Writes all relative file path flags' parameters. Args: hfile: A file handler instance. rel_paths_dict: A dict contains data of flag as a key and the relative path string as its value. tag: A string of tag, such as 'CMAKE_C_FLAGS'. """ for flag, path in rel_paths_dict.items(): hfile.write( _SET_RELATIVE_PATH.format(tag, _add_dollar_sign(tag), flag, _build_cmake_path(path))) @common_util.check_args(hfile=(TextIOWrapper, StringIO), flags=list, tag=str) @common_util.io_error_handle def _write_all_flags(hfile, flags, tag): """Wrties all flags to the project file. Args: hfile: A file handler instance. flags: A list of flag strings to be added. tag: A string to be added a dollar sign. """ for flag in flags: hfile.write(_SET_ALL_FLAGS.format(tag, _add_dollar_sign(tag), flag)) def _add_dollar_sign(tag): """Adds dollar sign to a string, e.g.: 'ANDROID_ROOT' -> '${ANDROID_ROOT}'. Args: tag: A string to be added a dollar sign. Returns: A dollar sign added string. """ return ''.join(['${', tag, '}']) def _build_cmake_path(path, tag=''): """Joins tag, '${ANDROID_ROOT}' and path into a new string. Args: path: A string of a path. tag: A string to be added in front of a dollar sign Returns: The composed string. """ return ''.join([tag, _ANDROID_ROOT_SYMBOL, os.path.sep, path]) @common_util.check_args(hfile=(TextIOWrapper, StringIO), is_system=bool) @common_util.io_error_handle def _write_all_includes(hfile, includes, is_system): """Writes all included directories' paths to the CLion project file. Args: hfile: A file handler instance. includes: A list of included file paths. is_system: A boolean of whether it's a system flag. """ if not includes: return system = '' if is_system: system = _SYSTEM hfile.write(_INCLUDE_DIR.format(system)) for include in includes: hfile.write(_SET_INCLUDE_FORMAT.format(_build_cmake_path(include))) hfile.write(_END_WITH_TWO_BLANK_LINES) @common_util.check_args(hfile=(TextIOWrapper, StringIO)) @common_util.io_error_handle def _write_all_headers(hfile, includes): """Writes all header directories' paths to the CLion project file. Args: hfile: A file handler instance. includes: A list of included file paths. """ if not includes: return hfile.write(_GLOB_RECURSE_TMP_HEADERS) for include in includes: hfile.write(_ALL_HEADER_FILES.format(_build_cmake_path(include))) hfile.write(_END_WITH_ONE_BLANK_LINE) hfile.write(_APPEND_SOURCE_FILES) def _cleanup_executable_name(mod_name): """Clean up an executable name to be suitable for CMake. Replace the last '@' of a module name with '-' and make it become a suitable executable name for CMake. Args: mod_name: A string of module name to be cleaned up. Returns: A string of the executable name. """ return mod_name[::-1].replace('@', '-', 1)[::-1]