【问题标题】:Have CMake rebuild custom targets when transitive dependency outdated当传递依赖过时时,让 CMake 重建自定义目标
【发布时间】:2024-01-04 16:02:02
【问题描述】:

我有一个多目录 CMake 项目,我在其中生成两个文件 bc。然后我有一个需要c 的可执行文件d_target

  • 文件b依赖于一些配置文件a
  • 文件c 依赖于文件b
  • 可执行目标d_target需要c,这是一个生成的头文件

我添加了两个add_custom_command() 调用来生成文件并添加了依赖项。 然后,我添加了两个 add_custom_target() 调用以使目标在整个多目录项目中可用。

file a > file b > b_target > file c > c_target > d_target

我第一次构建c_target,所有的依赖都被构建,文件c等于bb等于a

现在,当我更改“配置”a 并构建 d_target 时,d_target 不会重新构建。

当 CMake 的任何依赖项发生更改时,如何让 CMake 重建目标?

cmake_minimum_required(VERSION 3.15)

project(abcd)

# ./b/CMakeLists.txt
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/b
  COMMAND cat ${CMAKE_CURRENT_SOURCE_DIR}/a > ${CMAKE_CURRENT_BINARY_DIR}/b
  DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/a)
add_custom_target(b_target DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/b)

# ./c/CMakeLists.txt
add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/c
  COMMAND cat ${CMAKE_CURRENT_BINARY_DIR}/b > ${CMAKE_CURRENT_BINARY_DIR}/c
  DEPENDS b_target ${CMAKE_CURRENT_BINARY_DIR}/b)
add_custom_target(c_target DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/c)

# ./d/CMakeLists.txt
add_executable(d_target ${CMAKE_CURRENT_SOURCE_DIR}/d.cpp)
add_dependencies(d_target c_target) 
# will not generate:
# add_dependencies(d_target c_target  ${CMAKE_CURRENT_BINARY_DIR}/c)

在我的具体情况下,bcd 的命令在各自子目录的 CMakeLists.txt 中。

演示:

$ make d_target
[ 20%] Generating b
[ 20%] Built target b_target
[ 40%] Generating c
[ 60%] Built target c_target
[ 80%] Building CXX object CMakeFiles/d_target.dir/d.cpp.o
[100%] Linking CXX executable d_target
[100%] Built target d_target
$ touch ../a
$ make d_target
[ 20%] Generating b
[ 20%] Built target b_target
[ 40%] Generating c
[ 60%] Built target c_target
[100%] Built target d_target

当我尝试将c 文件添加到add_dependencies(d_target ...) 时,CMake 失败:

CMake Error at CMakeLists.txt:21 (add_dependencies):
  The dependency target "/home/tom/tmp/abcd/build/c" of target
  "d_target" does not exist.

【问题讨论】:

  • 正如当前代码中所写,b/CMakeLists.txt 中的add_custom_command 将在b 的构建目录中创建b。但是c/CMakeLists.txt 中的${CMAKE_CURRENT_BINARY_DIR}/b 指的是c 的构建目录中的b。所以你在不同的目录中有不同的b 文件。
  • @Tsyvarev 感谢您的评论,我稍微澄清了这个例子,但这不是根本问题。
  • 哦,您需要在c/CMakeLists.txt 中添加${CMAKE_CURRENT_BINARY_DIR}/b 作为add_custom_command 的依赖项。否则,如果文件 b 已更改,CMake(和 Make)不知道您要重建文件 c。仅当目标是可执行文件或库(使用add_executableadd_library 调用创建)时,将目标指定为依赖项会自动添加文件级依赖项。由于您的 b_target 是自定义目标,因此它不会添加文件级依赖项。另请参阅add_custom_target 的文档。
  • 取决于文件,而不是目标。
  • "可执行目标 d_target 需要 c。" - 可执行目标d_target 是从单个源文件d.cpp 创建的。 为什么编译/链接d_target需要文件c如何文件c影响d_target的编译或链接?

标签: cmake gnu-make


【解决方案1】:

以下是c/CMakeLists.txt sn-p 中带有解释性 cmets 的解决方案。 您可以将 b/a 内的值更改为 SOME_DEFINE 并重新构建以查看更新。

根 CMakeLists.txt

cmake_minimum_required(VERSION 3.15)

project(abcd)

add_subdirectory(b)
add_subdirectory(c)
add_subdirectory(d)

b/a

#define SOME_DEFINE 4

b/CMakeLists.txt

set(B_INPUT "${CMAKE_CURRENT_SOURCE_DIR}/a")
set(B_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/b")

add_custom_command(
    OUTPUT ${B_OUTPUT}
    COMMAND ${CMAKE_COMMAND} -E copy ${B_INPUT} ${B_OUTPUT}
    DEPENDS ${B_INPUT}
    )

add_custom_target(b_target DEPENDS ${B_OUTPUT})

c/CMakeLists.txt

get_target_property(B_OUTPUT_DIR b_target BINARY_DIR)
set(C_INPUT "${B_OUTPUT_DIR}/b")
set(C_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/c")

add_custom_command(
    OUTPUT ${C_OUTPUT}
    COMMAND ${CMAKE_COMMAND} -E copy ${C_INPUT} ${C_OUTPUT}
    DEPENDS ${C_INPUT}
    )

# Using "_c_target" as a local target and intending the non underscore prefix
# for use outside this CMakeLists.txt file
add_custom_target(_c_target DEPENDS ${C_OUTPUT})

# This forces `b_target` to be built prior to `_c_target`.  Since the custom
# command to build `C_INPUT` is in a different CMakeLists, CMake doesn't
# understand that `C_INPUT` is created by a custom command and thus doesn't know
# to generate it.
# This is only needed because the custom command for `C_INPUT` is in another
# file.  If the custom command was here, in this file, CMake would implicitly
# hook up the dependency for us through the custom commands and we wouldn't need
# `b_target`.
add_dependencies(_c_target b_target)

# Since `C_OUTPUT` is a header file, we create a header only library that the
# executable can link to.  This header only library provides the include
# directory of the generated file.
add_library(c_target INTERFACE)
target_include_directories(c_target INTERFACE ${CMAKE_CURRENT_BINARY_DIR})

# Per docs for add_dependencies
#
#   Dependencies added to an imported target or an interface library are
#   followed transitively"
# 
# This ensures that the `_c_target` will be built prior to anything that may end
# up using `c_target`.
add_dependencies(c_target _c_target)

d/CMakeLists.txt

add_executable(d_target ${CMAKE_CURRENT_SOURCE_DIR}/d.cpp)
target_link_libraries(d_target PRIVATE c_target)

d/d.cpp

#include <stdio.h>

#include "c"

int main(int argc, char *argv[]) {
    printf("The value is %d.\n", SOME_DEFINE);
}

【讨论】: