如何仅在生成器或输入在CMake中发生更改时构建自动生成的代码

How to only build auto generated code when the generator or input changes in CMake?

本文关键字:构建 代码 自动生成 CMake 何仅 输入      更新时间:2023-10-16

我正在开发一个源代码存储库,该存储库通过运行输出标头和实现的python脚本来生成一些C++代码。此代码随后被编译并链接到我的库和可执行文件。我知道生成的代码只有在两个条件之一为真时才会改变:

  1. 生成器代码本身发生更改
  2. 生成器的输入(XML文件)发生更改

我想使用cmake来管理构建过程。目前,我正在使用execute_process来启动发电机。然而,每当我运行cmake时,它就会运行,并且它会接触到文件,导致我生成的代码被重新编译,并增加了我的总编译时间。

我还想确保生成的代码总是在我的库之前运行。换句话说,我希望库依赖于生成器来运行。

在cmake,处理这种情况的正确方法是什么?我以前看到过这样的回答:"在构建库之前,让CMake执行项目中的目标"。但这依赖于代码生成器的输出是事先已知的。我的代码生成器将生成数量可变的文件。

使用ADD_CUSTOM_COMMAND来触发生成器。它允许您定义输入和输出依赖关系,并且只有在输出比输入旧时才会运行。

ADD_CUSTOM_COMMAND( OUTPUT generatedfile1 generatedfile2
                    COMMAND python generateSources.py xmlfile1 xmlfile2
                    DEPENDS xmlfile1 xmlfile2 generateSources.py 
                    COMMENT "Generating source code from XML" )

请确保生成的文件不会在多个独立的目标中使用,这些目标可能会并行编译,或者在构建过程中可能会发生冲突。为了确保这一点,以下应该做到:

ADD_CUSTOM_TARGET( RunGenerator DEPENDS generatedfile1 generatedfile2 
                   COMMENT "Checking if re-generation is required" )

然后让你的其他目标依赖于这个:

ADD_DEPENDENCIES( MyTarget RunGenerator )

注意:RunGenerator目标将始终被视为过时,因此始终运行。然而,由于在这种情况下它什么都不做(除了打印注释和检查依赖项),所以这无关紧要。如果需要,自定义命令将负责再生。

评论后更新:

如果你不知道文件的名称,你可以使用

ADD_CUSTOM_COMMAND( OUTPUT generated.timestamp
                    COMMAND python generateSources.py xmlfile1 xmlfile2
                    COMMAND ${CMAKE_COMMAND} -E touch generated.timestamp
                    DEPENDS xmlfile1 xmlfile2 generateSources.py 
                    COMMENT "Generating source code from XML" )

但是:使用GLOB需要显式运行CMake来更新文件列表。将其集成到自定义命令中可能会打乱您的构建过程(如果多个项目并行构建,而一个项目重新启动CMake配置)。IIRC,当您知道python脚本或XML文件发生了更改时,手动运行CMake是可以的,但您的问题是,当其他任何事情需要重新运行CMake时,这些文件都会被触及。

如果python脚本运行时间不长,您可以让它在每次CMake运行时运行(就像现在一样),但要确保未更改的文件不会被触摸,您可以尝试以下操作(未经测试):

# generated sources files into a temporary directory (adjust your current execute_process)
EXECUTE_PROCESS( COMMAND python ../generateSources.py ../xmlfile1 ../xmlfile2 
                 WORKING_DIRECTORY tmp )
# get the filenames
FILE( GLOB GENERATED_TEMP_FILES tmp/* )
# copy to the "expected" directory, but only if content CHANGED
FOREACH( F ${GENERATED_TEMP_FILES} )
    GET_FILENAME_COMPONENT( "${F}" FN NAME)
    CONFIGURE_FILE( "${F}" "./generated/${FN}" COPY_ONLY )
ENDFOREACH()
# use your current globbing command
FILE( GLOB GENERATED_SOURCES ./generated/* )

一个低级别的解决方案在另一个目录中生成文件,将文件与当前文件进行比较,如果它们不同,只进行复制,不进行复制,也不重新编译。

当然,只有当生成器没有填充一些随机噪声(如版本和日期)时,这才起作用。在这种情况下,你可以试着把它们过滤掉。

我今天或多或少遇到了这个问题。我正在将二进制资源嵌入到c++可执行文件中(它是一个嵌入式web服务器)。

解决方法如下:

    #get the file timestamp of the 'source' resource file
    FILE(TIMESTAMP "${CMAKE_CURRENT_SOURCE_DIR}/${_resource}" RESOURCE_TIME)
    #get the file time of the 'template' file
    FILE(TIMESTAMP ${TEMPLATE_FILE} TEMPLATE_TIME)
    #get the timestamp (if any) of the generated file
    FILE(TIMESTAMP "${CMAKE_CURRENT_BINARY_DIR}/${_filename}" TARGET_TIME)
....
    #only configure the file if the target is older than either the
    #source or the template
    IF((RESOURCE_TIME > TARGET_TIME) OR (TEMPLATE_TIME > TARGET_TIME) OR (NOT TARGET_TIME))
        MESSAGE(STATUS "configuring ${...}")
        FILE(READ ${_resource} RESOURCE_CONTENT HEX)
        string(REGEX REPLACE "([0-9a-f][0-9a-f])" "0x\1," RESOURCE_CONTENT ${RESOURCE_CONTENT})
        configure_file("${TEMPLATE_FILE}"
            "${CMAKE_CURRENT_BINARY_DIR}/${_filename}"
            @ONLY)
    ELSE()
        MESSAGE(STATUS "already configured ${...}")
    ENDIF()

我工作的地方也遇到过类似的问题。只是我们不使用cmake,而是使用另一个构建系统。

我不知道cmake,但这可能会给你几个想法:

我们的解决方案包括将xml和生成器视为需要编译的代码,将生成器视为依赖项,将xml文件视为源。如果生成器或xml文件发生了更改,则构建系统会运行生成器(生成所有内容——即使只有一个文件发生了变化,也意味着它会触及所有生成的文件)。

当然,构建系统并没有直接运行生成器,而是我们编写了一个小的python脚本,决定如何正确运行生成器——例如,我们可以添加的一个改进是将所有文件生成到/tmp,并且只移动更改后的文件(使用diff进行比较),因此只有更改过的文件才会被触摸。(我们现在不需要它,因为我们的文件不会经常更改)

我们还使用两个不同的图运行了两次构建系统,一个图用于生成器,另一个图则用于其他文件。我们将其设计为允许多个级别的生成器生成依赖关系,以便一个生成器可以依赖于另一个生成器生成的产品。

另外两个需要考虑的技巧是,如果您的生成系统允许您使用正则表达式来生成文件,您可能需要使用它。此外,您可以在生成过程中生成生成系统的配置文件。