CMake中如何自动用compdb更新编译数据库或添加头文件?
问题背景
我们使用CMake结合CMAKE_EXPORT_COMPILE_COMMANDS生成供IDE及代码分析使用的编译数据库,但部分仅含头文件的库无法正常工作。由于编译数据库未包含头文件,我们尝试通过compdb工具添加头文件,该方法虽有效但属于额外步骤,我们希望避免这一步骤——尤其是CMake可能直接触发,或在文件变更时通过make/ninja触发,每次运行compdb耗时较高,成本不菲。
我们最新的尝试是使用cmake_language(DEFER)添加配置后钩子,但发现时机仍过早,更新后的compile_commands.json会在CMake结束前被再次覆盖,相关代码如下:
cmake_language(DEFER DIRECTORY ${CMAKE_SOURCE_DIR} CALL _compdb_update_compilation_database())
现咨询以下两个问题:
- 是否能够在CMake内部,在编译数据库生成完成后可靠地运行compdb?
- 是否存在其他方法可在配置阶段将头文件添加到编译数据库中?
问题1:CMake内部可靠运行compdb的时机
CMake本身没有原生提供编译数据库生成完成后的钩子,但有两种可靠的实现方式:
方法1:自定义POST_BUILD目标
通过创建自定义目标,将其绑定到主构建流程,确保compile_commands.json被CMake最终生成后再执行compdb:
add_custom_target( UpdateCompDB COMMAND compdb -p ${CMAKE_BINARY_DIR} list > ${CMAKE_BINARY_DIR}/compile_commands.json COMMENT "Updating compile_commands.json with header files" ) # 让主目标依赖该更新目标,确保每次构建后自动执行 add_dependencies(YourMainTarget UpdateCompDB)
如果只想在CMake配置阶段执行一次,而非每次构建都跑,可以在CMakeLists.txt末尾添加延迟执行的命令,确保所有内部生成步骤完成后再调用compdb:
# 配置阶段末尾执行,避开CMake覆盖编译数据库的时机 execute_process( COMMAND compdb -p ${CMAKE_BINARY_DIR} list > ${CMAKE_BINARY_DIR}/compile_commands.json WORKING_DIRECTORY ${CMAKE_BINARY_DIR} OUTPUT_QUIET ERROR_QUIET )
若要优化性能,仅在头文件或原始编译数据库变化时执行,可添加文件依赖检查:
file(GLOB_RECURSE HEADER_FILES ${CMAKE_SOURCE_DIR}/*.h ${CMAKE_SOURCE_DIR}/*.hpp) add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/compile_commands_updated.json COMMAND compdb -p ${CMAKE_BINARY_DIR} list > ${CMAKE_BINARY_DIR}/compile_commands_updated.json COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_BINARY_DIR}/compile_commands_updated.json ${CMAKE_BINARY_DIR}/compile_commands.json DEPENDS ${CMAKE_BINARY_DIR}/compile_commands.json ${HEADER_FILES} ) add_custom_target(UpdateCompDB ALL DEPENDS ${CMAKE_BINARY_DIR}/compile_commands_updated.json)
方法2:覆盖CMake内部编译数据库生成函数
CMake的CMAKE_EXPORT_COMPILE_COMMANDS依赖内部_WriteCompileCommands函数实现,你可以重写这个函数,在原始逻辑执行后立刻调用compdb:
function(WriteCompileCommands) # 调用CMake原生的编译数据库生成逻辑 _WriteCompileCommands(${ARGN}) # 紧接着执行compdb更新 execute_process( COMMAND compdb -p ${CMAKE_BINARY_DIR} list > ${CMAKE_BINARY_DIR}/compile_commands.json WORKING_DIRECTORY ${CMAKE_BINARY_DIR} ) endfunction()
注意:这种方式依赖CMake内部函数命名,不同版本可能存在兼容性问题,需测试验证。
问题2:配置阶段直接添加头文件到编译数据库
无需依赖compdb,CMake本身有几种方式在配置阶段就将头文件纳入编译数据库:
方法1:为头文件创建伪编译目标
对仅含头文件的库,创建一个不生成实际输出的伪目标,让CMake为其生成编译条目:
add_library(HeaderOnlyLib INTERFACE) target_include_directories(HeaderOnlyLib INTERFACE ${CMAKE_SOURCE_DIR}/include) # 创建伪目标,模拟头文件编译(实际不生成目标文件) add_custom_target( HeaderOnlyCompileCommands COMMAND ${CMAKE_CXX_COMPILER} -x c++-header -std=c++17 ${CMAKE_SOURCE_DIR}/include/*.hpp -I${CMAKE_SOURCE_DIR}/include COMMENT "Generating compile commands for header-only library" VERBATIM )
CMake会自动将这个伪目标的编译命令写入compile_commands.json,从而包含头文件的编译信息。
方法2:手动生成编译数据库条目
在配置阶段直接生成包含头文件的条目,追加到编译数据库中:
# 获取所有头文件列表 file(GLOB_RECURSE HEADER_FILES ${CMAKE_SOURCE_DIR}/include/*.hpp) # 生成头文件对应的编译条目字符串 set(HEADER_COMPILE_COMMANDS "") foreach(HEADER ${HEADER_FILES}) string(APPEND HEADER_COMPILE_COMMANDS ",\n { \"directory\": \"${CMAKE_BINARY_DIR}\", \"command\": \"${CMAKE_CXX_COMPILER} -x c++-header -std=c++17 -I${CMAKE_SOURCE_DIR}/include ${HEADER}\", \"file\": \"${HEADER}\" }") endforeach() # 生成最终的编译数据库文件 file(GENERATE OUTPUT ${CMAKE_BINARY_DIR}/compile_commands.json CONTENT "[ $<JOIN:$<TARGET_PROPERTY:YourMainTarget,COMPILE_COMMANDS>,\n > ${HEADER_COMPILE_COMMANDS} ]" )
这种方式适合需要精细控制头文件编译选项的场景,大型项目也能通过file(GLOB_RECURSE)自动遍历头文件。
方法3:将头文件作为自定义命令依赖
通过将头文件设为自定义命令的依赖,让CMake将其纳入编译数据库:
file(GLOB_RECURSE HEADER_FILES ${CMAKE_SOURCE_DIR}/include/*.hpp) add_custom_command( OUTPUT ${HEADER_FILES} COMMAND ${CMAKE_COMMAND} -E touch ${HEADER_FILES} MAIN_DEPENDENCY ${HEADER_FILES} ) add_custom_target(HeaderFiles ALL DEPENDS ${HEADER_FILES})
CMake会为这些头文件生成对应的编译条目(实际仅执行touch操作,但编译命令会包含头文件路径和必要的包含选项)。
内容的提问来源于stack exchange,提问作者Simon Kraemer

