在 CMake 中使用源代码生成器的正确方法是什么
What is the proper way of using a source generator in CMake
在我的 C++ 项目中,我使用源代码生成器将一些资源嵌入到二进制文件中。
我使用 CMake 来构建我的项目,我的代码可以工作,但有一些问题。
我很确定我想完成的事情是可能的,但我没有在网上找到任何答案。
我目前遇到的问题是:
生成器每次都会运行,即使输入文件没有改变。
这没什么大不了的,因为它真的很快,但我希望有更好的方法来做到这一点
在使用 Ninja
时,生成器会在每次构建时运行(如上所述),而无需每次都重建。
我认为 Ninja 看到该文件没有更改并且不会再次构建它,
但是当我对资源进行更改时,它仍然使用旧版本。
需要另一个构建才能“意识到”生成的文件已更改并重建它
使用Make
代码每次都会重建,即使生成的文件没有改变,导致构建时间浪费
在这两种情况下(查看输出)生成器在编译器之前运行。
这种情况并非不可持续,但我想知道是否有更好的解决方案。
这是我的 CMakeLists.txt
中的代码片段
add_subdirectory(Generator)
file(GLOB RESOURCES Resources/*)
add_custom_command(
OUTPUT src/Resources/Generated.hpp src/Resources/Generated.cpp
COMMAND Generator ${RESOURCES}
DEPENDS ${RESOURCES}
DEPENDS Generator
)
add_custom_target(Generated DEPENDS src/Resources/Generated.hpp src/Resources/Generated.cpp)
add_dependencies(${PROJECT_NAME} Generated)
Here's a minimal reproducible example,抱歉之前没有。
编辑:我在同一个仓库的 fix
分支中根据正确答案实现了功能解决方案。它可能对将来的参考有用:)
这个很有趣,因为有 多个 错误和文体问题,它们部分重叠。
首发:
file(GLOB_RECURSE SRC src/*.cpp src/*.hpp)
add_executable(${PROJECT_NAME} ${SRC})
虽然一开始很方便,但遍历您的资源并不是一个好主意。在某些时候,您将在那里有一个 testme.cpp
不应该与其余部分一起构建,或者一个 conditionally_compiled.cpp
应该只在设置了某个选项的情况下编译。您最终编译了您真正不打算编译的资源。
在这种情况下,文件 src/Generated.hpp
来自您的 git 存储库。该文件应该是 生成的 ,而不是从 repo 中检出的。它是怎么进去的?
add_custom_command(
OUTPUT src/Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh
${PROJECT_SOURCE_DIR}/Resources/data.txt
> ${PROJECT_SOURCE_DIR}/src/Generated.hpp
DEPENDS Resources/data.txt
DEPENDS ${PROJECT_SOURCE_DIR}/generator.sh
)
你看到那里的输出重定向了吗?您写信给 ${PROJECT_SOURCE_DIR}
。那不是一个好主意。你的源代码树永远不应该有任何编译或生成的东西。因为这些事情最终会与其他事情一起发生......就像发生在你身上一样。
下一期:
add_custom_target(Generated DEPENDS src/Generated.hpp)
这将创建一个 make
目标 Generated
。试试看:make Generated
。您不断得到以下输出:
[100%] Generating src/Generated.hpp
[100%] Built target Generated
显然它没有意识到 Generated.hpp
已经是 up-to-date。为什么不呢?
让我们再看看您的自定义命令:
add_custom_command(
OUTPUT src/Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh
${PROJECT_SOURCE_DIR}/Resources/data.txt
> ${PROJECT_SOURCE_DIR}/src/Generated.hpp
DEPENDS Resources/data.txt
DEPENDS ${PROJECT_SOURCE_DIR}/generator.sh
)
如果我告诉你你的 OUTPUT
从未真正生成过呢?
引用自CMake docs on add_custom_command
,强调我的:
OUTPUT
Specify the output files the command is expected to produce. If an output name is a relative path it will be interpreted relative to the build tree directory corresponding to the current source directory.
所以你的输出声称是 binary 树,但你的命令重定向到 source 树......难怪生成器不断得到 re-run.
在错误的位置生成 header 不会给您带来 编译器 错误,因为 header 来自您的 源 树被你的 GLOB_RECURSE
捡起。随着那个不断得到 re-generated,您的可执行文件也不断得到重新编译。
从你的构建目录试试这个:
mkdir src && touch src/Generated.hpp && make Generated
输出:
[100%] Built target Generated
您看到 make
与 Generated
目标无关,因为它现在看到您的自定义命令的 OUTPUT
比其依赖项更新。当然,touch
ed 文件不是生成的文件;我们需要将它们整合在一起。
解决方案
不要写入您的源代码树。
由于 ${PROJECT_BINARY_DIR}/src
不存在,您需要创建它,或者将创建的文件放在顶层目录中。为了简单起见,我在这里做了后者。我还删除了不必要的 ${PROJECT_SOURCE_DIR}
用途。
add_custom_command(
OUTPUT Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh \
${PROJECT_SOURCE_DIR}/Resources/data.txt \
> Generated.hpp
DEPENDS Resources/data.txt
DEPENDS generator.sh
)
不要 glob,控制实际编译的内容:
add_executable( ${PROJECT_NAME} src/main.cpp )
将二叉树添加到目标的包含路径中。 add_executable
之后:
target_include_directories( ${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR} )
就是这样,现在一切正常。
在我的 C++ 项目中,我使用源代码生成器将一些资源嵌入到二进制文件中。
我使用 CMake 来构建我的项目,我的代码可以工作,但有一些问题。 我很确定我想完成的事情是可能的,但我没有在网上找到任何答案。
我目前遇到的问题是:
生成器每次都会运行,即使输入文件没有改变。 这没什么大不了的,因为它真的很快,但我希望有更好的方法来做到这一点
在使用
Ninja
时,生成器会在每次构建时运行(如上所述),而无需每次都重建。 我认为 Ninja 看到该文件没有更改并且不会再次构建它, 但是当我对资源进行更改时,它仍然使用旧版本。 需要另一个构建才能“意识到”生成的文件已更改并重建它使用
Make
代码每次都会重建,即使生成的文件没有改变,导致构建时间浪费
在这两种情况下(查看输出)生成器在编译器之前运行。
这种情况并非不可持续,但我想知道是否有更好的解决方案。
这是我的 CMakeLists.txt
add_subdirectory(Generator)
file(GLOB RESOURCES Resources/*)
add_custom_command(
OUTPUT src/Resources/Generated.hpp src/Resources/Generated.cpp
COMMAND Generator ${RESOURCES}
DEPENDS ${RESOURCES}
DEPENDS Generator
)
add_custom_target(Generated DEPENDS src/Resources/Generated.hpp src/Resources/Generated.cpp)
add_dependencies(${PROJECT_NAME} Generated)
Here's a minimal reproducible example,抱歉之前没有。
编辑:我在同一个仓库的 fix
分支中根据正确答案实现了功能解决方案。它可能对将来的参考有用:)
这个很有趣,因为有 多个 错误和文体问题,它们部分重叠。
首发:
file(GLOB_RECURSE SRC src/*.cpp src/*.hpp)
add_executable(${PROJECT_NAME} ${SRC})
虽然一开始很方便,但遍历您的资源并不是一个好主意。在某些时候,您将在那里有一个 testme.cpp
不应该与其余部分一起构建,或者一个 conditionally_compiled.cpp
应该只在设置了某个选项的情况下编译。您最终编译了您真正不打算编译的资源。
在这种情况下,文件 src/Generated.hpp
来自您的 git 存储库。该文件应该是 生成的 ,而不是从 repo 中检出的。它是怎么进去的?
add_custom_command(
OUTPUT src/Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh
${PROJECT_SOURCE_DIR}/Resources/data.txt
> ${PROJECT_SOURCE_DIR}/src/Generated.hpp
DEPENDS Resources/data.txt
DEPENDS ${PROJECT_SOURCE_DIR}/generator.sh
)
你看到那里的输出重定向了吗?您写信给 ${PROJECT_SOURCE_DIR}
。那不是一个好主意。你的源代码树永远不应该有任何编译或生成的东西。因为这些事情最终会与其他事情一起发生......就像发生在你身上一样。
下一期:
add_custom_target(Generated DEPENDS src/Generated.hpp)
这将创建一个 make
目标 Generated
。试试看:make Generated
。您不断得到以下输出:
[100%] Generating src/Generated.hpp
[100%] Built target Generated
显然它没有意识到 Generated.hpp
已经是 up-to-date。为什么不呢?
让我们再看看您的自定义命令:
add_custom_command(
OUTPUT src/Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh
${PROJECT_SOURCE_DIR}/Resources/data.txt
> ${PROJECT_SOURCE_DIR}/src/Generated.hpp
DEPENDS Resources/data.txt
DEPENDS ${PROJECT_SOURCE_DIR}/generator.sh
)
如果我告诉你你的 OUTPUT
从未真正生成过呢?
引用自CMake docs on add_custom_command
,强调我的:
OUTPUT
Specify the output files the command is expected to produce. If an output name is a relative path it will be interpreted relative to the build tree directory corresponding to the current source directory.
所以你的输出声称是 binary 树,但你的命令重定向到 source 树......难怪生成器不断得到 re-run.
在错误的位置生成 header 不会给您带来 编译器 错误,因为 header 来自您的 源 树被你的 GLOB_RECURSE
捡起。随着那个不断得到 re-generated,您的可执行文件也不断得到重新编译。
从你的构建目录试试这个:
mkdir src && touch src/Generated.hpp && make Generated
输出:
[100%] Built target Generated
您看到 make
与 Generated
目标无关,因为它现在看到您的自定义命令的 OUTPUT
比其依赖项更新。当然,touch
ed 文件不是生成的文件;我们需要将它们整合在一起。
解决方案
不要写入您的源代码树。
由于 ${PROJECT_BINARY_DIR}/src
不存在,您需要创建它,或者将创建的文件放在顶层目录中。为了简单起见,我在这里做了后者。我还删除了不必要的 ${PROJECT_SOURCE_DIR}
用途。
add_custom_command(
OUTPUT Generated.hpp
COMMAND ${PROJECT_SOURCE_DIR}/generator.sh \
${PROJECT_SOURCE_DIR}/Resources/data.txt \
> Generated.hpp
DEPENDS Resources/data.txt
DEPENDS generator.sh
)
不要 glob,控制实际编译的内容:
add_executable( ${PROJECT_NAME} src/main.cpp )
将二叉树添加到目标的包含路径中。 add_executable
之后:
target_include_directories( ${PROJECT_NAME} PRIVATE ${PROJECT_BINARY_DIR} )
就是这样,现在一切正常。