使用CMake将文件从源目录复制到二进制目录

181

我正在尝试在CLion上创建一个简单的项目。它使用CMake生成Makefiles来构建项目(或某种类型的项目)

我需要做的就是每次运行我的代码时将一些非项目文件(某种资源文件)转移到二进制目录中。

该文件包含测试数据,应用程序打开它以读取它们。我尝试了几种方法:

  • 通过file(COPY ...

file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/input.txt
        DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/input.txt

看起来不错,但只工作一次,无法在下一次运行后重新复制文件。

  • 通过add_custom_command

    • OUTPUT版本

  • add_custom_command(
            OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/input.txt
            COMMAND ${CMAKE_COMMAND} -E copy
                    ${CMAKE_CURRENT_SOURCE_DIR}/input.txt
                    ${CMAKE_CURRENT_BINARY_DIR}/input.txt)
    
  • TARGET 版本

  • add_custom_target(foo)
    add_custom_command(
            TARGET foo
            COMMAND ${CMAKE_COMMAND} copy
                    ${CMAKE_CURRENT_BINARY_DIR}/test/input.txt
                    ${CMAKE_SOURCE_DIR})
    

    但是它们都没有起作用。

    我做错了什么?

    10个回答

    215

    您可以考虑使用configure_file命令并选择COPYONLY选项:

    configure_file(<input> <output> COPYONLY)
    

    file(COPY ...)不同的是,它在输入和输出之间创建了文件级别的依赖关系,即:

    如果修改输入文件,则构建系统将重新运行CMake以重新配置文件并再次生成构建系统。


    14
    请注意,即使您使用 GLOB 创建文件列表,configure_file 也无法处理子目录中的文件。 - Tarantula
    6
    请注意,这不是输入和输出之间的“文件级依赖关系”,而是输入与 cmake 配置步骤 之间的依赖关系。因此,这不像常规构建依赖关系,其中更改源文件仅会重新构建相应的对象文件,而是导致整个 cmake 重新配置,可能带来其他副作用。 - Seth Johnson
    1
    我们如何使用这个命令来递归复制目录文件? - H.M

    114

    这两个选项都是有效的,它们针对构建过程的两个不同步骤:

    1. file(COPY ... 命令在配置阶段复制文件,并且仅在此阶段执行。当您重新构建项目而没有更改cmake配置时,此命令不会被执行。
    2. add_custom_command 是每个构建步骤中复制文件的首选方法。

    对于您的任务,正确的版本应该是:

    add_custom_command(
            TARGET foo POST_BUILD
            COMMAND ${CMAKE_COMMAND} -E copy
                    ${CMAKE_SOURCE_DIR}/test/input.txt
                    ${CMAKE_CURRENT_BINARY_DIR}/input.txt)
    
    你可以在 PRE_BUILDPRE_LINKPOST_BUILD 之间进行选择。最好是阅读add_custom_command的文档。这里有一个示例,展示了如何使用第一个版本:Use CMake add_custom_command to generate source for another target

    1
    是CMAKE_SOURCE_DIR还是CMAKE_CURRENT_SOURCE_DIR? - Syaiful Nizam Yahya
    4
    如果 test/input.txt 文件相对于当前的 CMakeLists.txt 文件,则使用 CMAKE_CURRENT_SOURCE_DIR。如果它相对于根目录的 CMakeLists.txt 文件,则使用 CMAKE_SOURCE_DIR。请注意,不要改变原文的含义。 - Mark Ingram
    有没有一种方法可以在cmake中获取目标文件名,而不是硬编码文件名?例如,我正在构建一个lib文件,cmake是否有一个变量来获取目标文件名,比如libSomething.so - Hossein
    2
    注意:如果要递归复制目录而不是文件,请使用${CMAKE_COMMAND} -E copy_directory - jorisv92
    @jorisv92 你如何强制复制或者在使用copy_directory或其他递归操作时进行复制(如果不同)? - hiradyazdan

    26

    你尝试的第一个选项之所以不起作用,原因有两个。

    首先,你忘记了关闭括号。

    其次,DESTINATION 应该是一个目录而不是一个文件名。假设你关闭了括号,那么文件将会被保存在一个名为 input.txt 的文件夹中。

    要让它起作用,只需将其更改为:

    file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/input.txt
         DESTINATION ${CMAKE_CURRENT_BINARY_DIR})
    

    14
    我建议使用TARGET_FILE_DIR,如果你想将文件复制到与你的.exe文件相同的文件夹中。

    $ 主文件目录(.exe、.so.1.2、.a)。

    add_custom_command(
      TARGET ${PROJECT_NAME} POST_BUILD
      COMMAND ${CMAKE_COMMAND} -E copy 
        ${CMAKE_CURRENT_SOURCE_DIR}/input.txt 
        $<TARGET_FILE_DIR:${PROJECT_NAME}>)
    

    在VS中,这个CMake脚本将把input.txt复制到最终的exe文件所在的同一个文件夹中,无论是debug还是release。

    这很棒,但仅保证在POST_BUILD中可正常工作。 参考:https://gitlab.kitware.com/cmake/cmake/-/issues/21364#note_849331 - phcerdan
    只有这个方法对我有效。我正在将一个传统库从autoconf迁移到CMake。这个设置在使用gcc和msvc时都有效。 - undefined

    12

    建议使用configure_file可能是最简单的解决方案。但是,如果您手动从构建目录中删除了文件,则不会重新运行复制命令。为了处理这种情况,以下方法适用于我:

    add_custom_target(copy-test-makefile ALL DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/input.txt)
    add_custom_command(OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/input.txt
                       COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/input.txt
                                                        ${CMAKE_CURRENT_BINARY_DIR}/input.txt
                       DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/input.txt)
    

    1
    在我看来,这是唯一正确的解决方案。第一个命令告诉cmake需要存在哪些文件,而第二个命令告诉cmake如何创建它们。 - Watcom

    12

    如果你想将当前目录下的文件夹复制到二进制(构建)文件夹中

    file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/yourFolder/ DESTINATION ${CMAKE_CURRENT_BINARY_DIR}/yourFolder/)
    

    那么语法结构就是:
    file(COPY pathSource DESTINATION pathDistination)
    

    9

    以下是我用来复制一些资源文件的命令:

    copy-files是一个空目标,以忽略错误。

     add_custom_target(copy-files ALL
        COMMAND ${CMAKE_COMMAND} -E copy_directory
        ${CMAKE_BINARY_DIR}/SOURCEDIRECTORY
        ${CMAKE_BINARY_DIR}/DESTINATIONDIRECTORY
        )
    

    3
    我还会添加 add_dependencies(MainTarget copy-files),以便在构建 MainTarget 时自动运行。 - IC_
    这似乎是最好的答案(+Herrgott的评论),因为它确保源代码的当前版本始终在目标后构建中。对于小的复制作业,这很有效,谢谢。将add_dependencies(MainTarget copy-files)放在根CMakeLists.txt文件中意味着可以在整个项目中使用它。 - satnhak

    2

    单个文件

    首先添加一个复制的规则。 OUTPUT 指定生成的文件,DEPENDS 创建一个依赖关系,所以当输入发生变化时,该规则会再次运行。

    set(MY_RESOURCE_FILE input.txt)
    add_custom_command(
            OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${MY_RESOURCE_FILE}
            COMMAND ${CMAKE_COMMAND} -E copy
                ${CMAKE_CURRENT_SOURCE_DIR}/${MY_RESOURCE_FILE}
                ${CMAKE_CURRENT_BINARY_DIR}/${MY_RESOURCE_FILE}
            DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${MY_RESOURCE_FILE}
    )
    

    然后将“结果文件”作为目标所需的依赖项添加进去。
    add_executable(main_executable
        ${CMAKE_CURRENT_BINARY_DIR}/${MY_RESOURCE_FILE}
    
        src/main.cpp
    )
    

    这是一个现代而干净的CMake解决方案,所有依赖都是基于目标定义的,并根据需要向上传递到依赖树。我们不会改变任何全局状态。它还会跟踪文件本身,只有在过时时才会复制。这也使它成为一个强大而高效的解决方案。
    如果多个目标需要它,将它们添加到所有目标中。这是一个好的实践,因为依赖关系被明确地指定在需要它们的地方。
    多个文件
    由于这里有人要求复制多个文件,我也在这里添加了一些内容,因为还需要考虑一些其他事项。
    定义文件有两种可能性,可以手动列出它们,类似于上面的方法。或者我们可以创建一个递归的glob规则,自动发现它们。第一种方法非常直接,使用相对路径到CMAKE_CURRENT_SOURCE_DIR。
    set(MY_RESOURCE_FILES 
        resources/font.ttf
        resources/icon.svg
    )
    

    我还展示了第二个glob案例,需要更多的考虑。
    file(GLOB_RECURSE
        MY_RESOURCE_FILES
        RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} 
        CONFIGURE_DEPENDS
    
        ${CMAKE_CURRENT_SOURCE_DIR}/resources/**.ttf    # adapt these to your needs
        ${CMAKE_CURRENT_SOURCE_DIR}/resources/**.svg
    )
    

    指定CONFIGURE_DEPENDS会在任何目标过期时重新运行构建开始时的全局。而RELATIVE给出了相对于我们源目录的路径。并且使用两个星号**.svg可以递归搜索所有子目录。
    现在我们再次创建一个单独的规则来制作每个资源文件。
    FOREACH(MY_RESOURCE_FILE ${MY_RESOURCE_FILES})
        add_custom_command(
            OUTPUT ${CMAKE_CURRENT_BINARY_DIR}/${MY_RESOURCE_FILE}
            COMMAND ${CMAKE_COMMAND} -E copy
                ${CMAKE_CURRENT_SOURCE_DIR}/${MY_RESOURCE_FILE}
                ${CMAKE_CURRENT_BINARY_DIR}/${MY_RESOURCE_FILE}
            DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/${MY_RESOURCE_FILE}
      )
    ENDFOREACH()
    

    这样做还可以确保只复制缺失或更改的资源。
    最后,我们将所有这些添加到我们的可执行文件中。为此,我们只需要调整到二进制目录的路径。
    list(TRANSFORM MY_RESOURCE_FILES PREPEND ${CMAKE_CURRENT_BINARY_DIR}/)
    
    add_executable(${MAIN_TARGET} 
        ${MY_RESOURCE_FILES}
    
        src/main.cpp
    )
    

    1
    如果您不希望对二进制目录中的文件进行修改,您可以节省编写自定义命令/目标以在文件更改时重新复制的麻烦,而只需使用符号链接。CMake支持创建符号链接(或在Windows上几乎等效的操作)。它有file(CREATE_LINK <original> <linkname> [RESULT <result>] [COPY_ON_ERROR] [SYMBOLIC])命令和create_symlink <old> <new>命令行命令。
    如果您不希望源目录中的副本发生变化,或者每次发生变化时都可以手动重新配置,您可以使用file(COPY_FILE <oldname> <newname> [RESULT <result>] [ONLY_IF_DIFFERENT] [INPUT_MAY_BE_RECENT])file(COPY [...])命令。

    0

    如果你想在构建后将example的内容放入install文件夹中:

    code/
      src/
      example/
      CMakeLists.txt
    

    尝试将以下内容添加到您的CMakeLists.txt文件中:

    install(DIRECTORY example/ DESTINATION example)
    

    1
    但是'install'命令有一个副作用:安装的文件将出现在cpack包中... - gary133
    这是对不同问题的正确答案。 :) Build (cmake --build ...) 是与 install (cmake --install...) 分开的阶段。特别是,VSCode 通常仅运行构建命令并直接从 CMAKE_BINARY_DIR 调试程序。问题有点模糊,需要在哪个阶段(configure 或 build)复制文件,但安装阶段肯定太晚了。是的,这些文件将需要使用 install() 命令进行单独的安装描述,在现代 CMake 框架中最好使用目标属性,但这超出了范围。 - kkm

    网页内容由stack overflow 提供, 点击上面的
    可以查看英文原文,
    原文链接