【问题标题】:Is it better to specify source files with GLOB or each file individually in CMake?用 GLOB 指定源文件还是在 CMake 中单独指定每个文件更好?
【发布时间】:2025-12-13 16:50:01
【问题描述】:

CMake 提供了多种方法来指定目标的源文件。 一种是使用通配符(documentation),例如:

FILE(GLOB MY_SRCS dir/*)

另一种方法是单独指定每个文件。

首选哪种方式?通配符似乎很容易,但我听说它有一些缺点。

【问题讨论】:

    标签: cmake


    【解决方案1】:

    完全披露:我最初更喜欢 globbing 方法,因为它简单,但多年来我逐渐认识到,明确列出文件对于大型、多开发人员项目来说不太容易出错。

    原答案:


    通配的优点是:

    • 添加新文件很容易,因为它们 只在一个地方列出:在 磁盘。不通配符创建 重复。

    • 您的 CMakeLists.txt 文件将是 更短。这是一个很大的优势,如果你 有很多文件。不通配符 导致您丢失 CMake 逻辑 在庞大的文件列表中。

    使用硬编码文件列表的优点是:

    • CMake 将正确跟踪磁盘上新文件的依赖关系 - 如果我们使用 glob 然后当你运行 CMake 时第一次没有被 glob 的文件将不会得到 捡到

    • 您确保只添加您想要的文件。 Globbing 可能会导致杂散 不需要的文件。

    为了解决第一个问题,您可以简单地“触摸”执行 glob 的 CMakeLists.txt,方法是使用 touch 命令或写入文件而不进行任何更改。这将强制 CMake 重新运行并获取新文件。

    要解决第二个问题,您可以将代码仔细组织到目录中,无论如何您都可能这样做。在最坏的情况下,您可以使用list(REMOVE_ITEM) 命令来清理文件的全局列表:

    file(GLOB to_remove file_to_remove.cpp)
    list(REMOVE_ITEM list ${to_remove})
    

    唯一可能让您感到困扰的实际情况是,如果您使用 git-bisect 之类的东西在同一构建目录中尝试旧版本的代码。在这种情况下,您可能需要清理和编译更多内容,以确保您在列表中获得正确的文件。这是一个极端的案例,而且你已经在你的脚趾上,这不是一个真正的问题。

    【讨论】:

    • 我认为这个答案掩盖了 cmake 缺少新文件的缺点,Simply "touch" the CMakeLists.txt 如果您是开发人员,则可以,但对于其他构建您的软件的人来说,您的构建确实是一个痛点更新后失败,他们有责任调查原因。
    • 你知道吗?自从 6 年前 写下这个答案后,我改变了一些想法,现在更喜欢明确列出文件。唯一真正的缺点是“添加文件需要更多工作”,但它可以为您省去各种麻烦。在很多方面,显式优于隐式。
    • 正如安东尼奥所说,投票是为了提倡“全球化”方法。改变答案的性质对于那些选民来说是一种诱饵和转换的事情。作为妥协,我添加了一个编辑以反映我改变的意见。我为在茶杯中引起这样的风暴向互联网道歉:-P
    • 我发现最好的折衷办法是使用一个很小的外部脚本(例如 python)来更新 CMakeLists.txt 文件的源代码。它结合了自动为您添加文件的好处(因此可以防止文件路径中的潜在拼写错误),但会生成一个可以检查到源代码控制的显式清单(因此增量是显而易见的/跟踪的并且 git-bisect 将起作用),允许检查在包含虚假或非预期文件时调试问题,并正确触发 cmake 以了解何时应该重新运行。
    • 通配还有一个缺点。这些文件最终以绝对路径的形式出现,因此,如果您想使用文件名(带有前导相对路径)在其他地方复制文件夹结构作为构建的一部分,突然间这些前导路径是绝对路径。
    【解决方案2】:

    在 CMake 中指定源文件的最佳方式是明确列出它们

    CMake 的创建者自己建议不要使用通配符。

    见:https://cmake.org/cmake/help/v3.15/command/file.html?highlight=glob#file

    (我们不建议使用 GLOB 从源代码树中收集源文件列表。如果在添加或删除源时没有更改 CMakeLists.txt 文件,则生成的构建系统无法知道何时要求 CMake 重新生成。 )

    当然,您可能想知道缺点是什么 - 请继续阅读!


    当 Globbing 失败时:

    通配的最大缺点是创建/删除文件不会自动更新构建系统。

    如果您是添加文件的人,这似乎是一个可以接受的权衡,但是这会给其他人构建您的代码带来问题,他们从版本控制更新项目,运行构建,然后联系您,抱怨
    “构建已损坏”。

    更糟糕的是,失败通常会导致一些链接错误,而这些错误并没有提供任何提示问题的原因,并且会浪费时间进行故障排除。

    在我参与的一个项目中,我们开始使用 globbing,但在添加新文件时收到很多投诉,因此明确列出文件而不是 globbing 就足够了。

    这也破坏了常见的 git 工作流程
    git bisect 和功能分支之间的切换)。

    所以我不推荐这个,它带来的问题远远超过了便利性,当有人因此无法构建你的软件时,他们可能会浪费很多时间来追查问题或者干脆放弃。

    还有一点,仅仅记住触摸CMakeLists.txt 并不总是足够的,对于使用通配的自动构建,我必须在每个构建之前运行cmake,因为文件可能 自上次构建以来已添加/删除*。

    规则的例外情况:

    有时更喜欢使用通配符:

    • 用于为不使用 CMake 的现有项目设置 CMakeLists.txt 文件。
      这是一种获取所有引用源的快速方法(一旦构建系统运行 - 将 globbing 替换为显式文件列表)。
    • 当 CMake 不用作 primary 构建系统时,例如,如果您正在使用不使用 CMake 的项目,并且您希望维护自己的构建系统为它。
    • 适用于文件列表经常更改以致无法维护的任何情况。在这种情况下它可能很有用,但是您必须接受运行cmake 以生成构建文件每次以获得可靠/正确的构建(这违背了 CMake 的意图——将配置与构建分离的能力)

    * 是的,我本可以编写代码来比较更新前后磁盘上的文件树,但这不是一个很好的解决方法,最好留给构建系统。 em>

    【讨论】:

    • “通配的最大缺点是创建新文件不会自动更新构建系统。”但是不是glob的话还是要手动更新CMakeLists.txt,意思是cmake还是不会自动更新构建系统?似乎无论哪种方式,您都必须记住手动执行某些操作才能构建新文件。触摸 CMakeLists.txt 似乎比打开并编辑它以添加新文件更容易。
    • @Dan,对于 您的 系统 - 当然,如果您只单独开发这很好,但是其他构建您项目的人呢?你打算给他们发电子邮件去手动触摸 CMake 文件吗?每次添加或删除文件时? - 在 CMake 中存储文件列表可确保构建始终使用 vcs 知道的相同文件。相信我 - 这不仅仅是一些微妙的细节 - 当许多开发人员的构建失败时 - 他们会邮寄列表并在 IRC 上询问代码是否损坏。注意:(即使在您自己的系统上,您也可能会返回 git 历史记录,例如,不要考虑进入并触摸 CMake 文件)
    • 啊我没想到这种情况。这是我听到的反对通配符的最好理由。我希望 cmake 文档能详细说明为什么他们建议人们避免使用通配符。
    • 我一直在考虑将最后一次 cmake 执行的时间戳写入文件的解决方案。唯一的问题是:1)它可能必须由 cmake 完成才能跨平台,因此我们需要避免 cmake 以某种方式第二次运行。 2)可能更多的合并冲突(顺便说一句,文件列表仍然会发生)在这种情况下,它们实际上可以通过采用稍后的时间戳来轻松解决。
    • @tim-mb, “但是如果 CMake 创建一个你可以签入的 filetree_updated 文件会很好,它会在每次文件更新时自动更改。” - 你刚刚准确地描述了我的回答。
    【解决方案3】:

    在 CMake 3.12 中,file(GLOB ...) and file(GLOB_RECURSE ...) 命令获得了一个CONFIGURE_DEPENDS 选项,如果 glob 的值发生更改,它将重新运行 cmake。 由于这是源文件通配的主要缺点,现在可以这样做了:

    # Whenever this glob's value changes, cmake will rerun and update the build with the
    # new/removed files.
    file(GLOB_RECURSE sources CONFIGURE_DEPENDS "*.cpp")
    
    add_executable(my_target ${sources})
    

    但是,有些人仍然建议避免使用通配符查找来源。事实上,the documentation 表示:

    我们不建议使用 GLOB 从源代码树中收集源文件列表。 ... CONFIGURE_DEPENDS 标志可能无法在所有生成器上可靠地工作,或者如果将来添加不能支持它的新生成器,使用它的项目将被卡住。即使CONFIGURE_DEPENDS 工作可靠,每次重建时执行检查仍然需要成本。

    就个人而言,我认为不必手动管理源文件列表的好处超过了可能的缺点。如果您确实必须切换回手动列出的文件,只需打印全局源列表并将其粘贴回即可轻松实现。

    【讨论】:

    • 如果您的构建系统执行完整的 cmake 和构建周期(删除构建目录,从那里运行 cmake 然后调用 makefile),只要它们不拉入不需要的文件,肯定没有使用 GLOBbed 资源的缺点?以我的经验,cmake 部分的运行速度比构建快得多,所以无论如何它的开销并不大
    • 真正的、深思熟虑的答案,总是在页面底部附近。那些宁愿继续更新文件的人没有注意到,当文件被手动更新时,实际的效率损失不是以秒为单位,或者在执行检查时实际上是纳秒,而可能是几天或几周 i> 当程序员在弄乱文件时失去他们的流程,或者完全推迟他们的工作,只是因为他们不想更新它。感谢您的回答,真正为人类服务……感谢 CMake 的人们终于修补了它! :)
    • “可能累积几天或几周,因为程序员在弄乱文件时会失去他们的流程”——需要引用
    • @AlexReinking 不要忘记@你正在回复的用户。
    • @S.ExchangeConsideredHarmful 我已经看到由于 globbing 导致您需要对文件列表进行数百万次更新来弥补它,因此在损坏的构建上浪费了很多时间。它不会经常发生,但1)每次发生都会立即引发错误报告和令人头疼的问题,并且2)这个问题足够频繁,以至于人们工作在这些项目上,只要有东西没有构建,现在总是从清除 cmake 缓存和重建开始。这种习惯每周浪费几个小时,这是使用文件(GLOB)的直接后果。
    【解决方案4】:

    您可以安全地 glob(并且可能应该)以额外的文件为代价来保存依赖项。

    在某处添加这样的功能:

    # Compare the new contents with the existing file, if it exists and is the 
    # same we don't want to trigger a make by changing its timestamp.
    function(update_file path content)
        set(old_content "")
        if(EXISTS "${path}")
            file(READ "${path}" old_content)
        endif()
        if(NOT old_content STREQUAL content)
            file(WRITE "${path}" "${content}")
        endif()
    endfunction(update_file)
    
    # Creates a file called CMakeDeps.cmake next to your CMakeLists.txt with
    # the list of dependencies in it - this file should be treated as part of 
    # CMakeLists.txt (source controlled, etc.).
    function(update_deps_file deps)
        set(deps_file "CMakeDeps.cmake")
        # Normalize the list so it's the same on every machine
        list(REMOVE_DUPLICATES deps)
        foreach(dep IN LISTS deps)
            file(RELATIVE_PATH rel_dep ${CMAKE_CURRENT_SOURCE_DIR} ${dep})
            list(APPEND rel_deps ${rel_dep})
        endforeach(dep)
        list(SORT rel_deps)
        # Update the deps file
        set(content "# generated by make process\nset(sources ${rel_deps})\n")
        update_file(${deps_file} "${content}")
        # Include the file so it's tracked as a generation dependency we don't
        # need the content.
        include(${deps_file})
    endfunction(update_deps_file)
    

    然后进行 globbing:

    file(GLOB_RECURSE sources LIST_DIRECTORIES false *.h *.cpp)
    update_deps_file("${sources}")
    add_executable(test ${sources})
    

    您仍然像以前一样围绕显式依赖项(并触发所有自动构建!),只是它位于两个文件中,而不是一个。

    过程中的唯一变化是在您创建了一个新文件之后。如果您不使用 glob,则工作流程是从 Visual Studio 内部修改 CMakeLists.txt 并重新构建,如果您使用 glob,则显式运行 cmake - 或者只需触摸 CMakeLists.txt。

    【讨论】:

    • 起初我以为这是一个在添加源文件时会自动更新 Makefile 的工具,但现在我明白了它的价值。好的!这解决了有人从存储库更新并让make 给出奇怪的链接器错误的担忧。
    • 我相信这可能是一个好方法。当然还是要记得在添加或删除文件后触发cmake,并且还需要提交这个依赖文件,所以对用户端进行一些教育是必要的。主要缺点可能是这个依赖文件可能会引发讨厌的合并冲突,如果不再次要求开发人员对该机制有一定的了解,这可能很难解决。
    • 如果您的项目有条件包含文件(例如,某些文件仅在启用某个功能时使用,或者仅用于特定操作系统),这将不起作用。对于便携式软件来说,某些文件仅用于特定平台是很常见的。
    【解决方案5】:

    单独指定每个文件!

    我使用传统的 CMakeLists.txt 和 python 脚本来更新它。添加文件后,我手动运行 python 脚本。

    在这里查看我的答案: https://*.com/a/48318388/3929196

    【讨论】:

      最近更新 更多