如何在依赖文件变更时重新运行CMake自定义命令?
解决CMake自定义命令依赖头文件变更的问题
你当前的CMake脚本只把源文件作为自定义命令的依赖,但源文件包含的头文件变更时不会触发脚本重新运行。针对Ninja构建系统,这里有几个实用的解决方法,不用手动递归解析头文件:
方案1:借助编译命令数据库自动生成DEPFILE
这是最省心的方案,利用CMake导出的编译命令来准确获取源文件的所有依赖:
- 先在CMakeLists开头开启编译命令导出:
Ninja会在构建目录生成set(CMAKE_EXPORT_COMPILE_COMMANDS ON)compile_commands.json,里面包含每个源文件的完整编译参数和依赖关系。 - 修改你的Python脚本
myscript.py,让它读取compile_commands.json,筛选出目标源文件对应的条目,提取所有头文件依赖,然后生成符合Ninja格式的DEPFILE(格式为输出文件: 依赖文件1 依赖文件2 ...)。 - 更新
add_custom_command,添加DEPFILE参数,并把compile_commands.json也加入依赖:
这样Ninja会自动解析DEPFILE里的所有依赖,头文件变更时就会触发脚本重新运行。add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.cpp COMMAND ${Python3_EXECUTABLE} myscript.py --target-sources "${TARGET_SOURCES}" --depfile ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.d DEPENDS ${TARGET_SOURCES} ${CMAKE_BINARY_DIR}/compile_commands.json VERBATIM)
方案2:用CMake内置工具扫描依赖(CMake 3.19+)
如果你的CMake版本在3.19以上,可以直接用cmake -E scan_deps命令提取源文件依赖,不用改Python脚本:
- 定义一个自定义函数来扫描单个源文件的依赖:
function(get_source_deps source_file deps_var) execute_process( COMMAND ${CMAKE_COMMAND} -E scan_deps ${source_file} WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR} OUTPUT_VARIABLE DEPS_OUTPUT OUTPUT_STRIP_TRAILING_WHITESPACE ) string(REPLACE "\n" ";" DEPS_LIST ${DEPS_OUTPUT}) set(${deps_var} ${DEPS_LIST} PARENT_SCOPE) endfunction() - 遍历所有目标源文件,收集所有依赖(包括头文件):
set(ALL_DEPENDS ${TARGET_SOURCES}) foreach(src ${TARGET_SOURCES}) get_source_deps(${src} SRC_DEPS) list(APPEND ALL_DEPENDS ${SRC_DEPS}) endforeach() - 把收集到的所有依赖替换原来的
${TARGET_SOURCES}:
注意:如果源文件用到了自定义的add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.cpp COMMAND ${Python3_EXECUTABLE} myscript.py DEPENDS ${ALL_DEPENDS} VERBATIM)INCLUDE_DIRECTORIES或预处理器宏,需要给scan_deps添加对应的-I、-D参数,否则可能漏解析部分头文件。
方案3:Python脚本调用Clang提取依赖
如果项目用Clang编译,可以直接在Python脚本里调用clang -MM命令生成依赖,这个命令的输出正好符合Ninja的DEPFILE格式:
- 修改
myscript.py,添加依赖提取逻辑:import subprocess import sys import os # 从命令行参数获取目标源文件、DEPFILE路径、输出文件路径 target_sources = sys.argv[1].split(';') depfile_path = sys.argv[2] output_file = sys.argv[3] all_deps = set() for src in target_sources: # 调用clang -MM生成依赖,可根据需要添加-I、-D等编译参数 result = subprocess.run( ['clang', '-MM', src], capture_output=True, text=True, cwd=os.getcwd() ) # 解析输出,提取所有头文件依赖 dep_part = result.stdout.split(':')[1].strip() all_deps.update(dep_part.split()) # 写入DEPFILE with open(depfile_path, 'w') as f: f.write(f"{output_file}: {' '.join(all_deps)}\n") # 原来的脚本逻辑... - 更新CMake的自定义命令,添加DEPFILE参数:
add_custom_command( OUTPUT ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.cpp COMMAND ${Python3_EXECUTABLE} myscript.py "${TARGET_SOURCES}" ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.d ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.cpp DEPENDS ${TARGET_SOURCES} DEPFILE ${CMAKE_BINARY_DIR}/static_init/generated/${target}/static_init.d VERBATIM)
推荐优先用方案1或3,前者能完美匹配CMake的编译配置,后者和实际编译的依赖解析逻辑完全一致,都能避免手动解析头文件的麻烦。
内容的提问来源于stack exchange,提问作者Crazy Sage
相关产品推荐
相关产品推荐

