最好使用GLOB指定源文件,还是在CMake中分别指定每个文件?


157

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

FILE(GLOB MY_SRCS dir/*)

另一种方法是分别指定每个文件。

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

Answers:


185

完全公开:我本来是喜欢使用通配方法,因为它简单易行,但多年来,我逐渐认识到,对于大型的,多开发人员的项目,明确列出文件不太容易出错。

原始答案:


遍历的优点是:

  • 添加新文件很容易,因为它们仅在一个位置列出:磁盘上。不阻塞会造成重复。

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

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

  • CMake将正确跟踪磁盘上新文件的依赖关系-如果我们使用glob,则在您运行CMake时第一次没有被glob的文件将不会被拾取

  • 您确保仅添加所需的文件。混淆可能会拾取您不想要的杂散文件。

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

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

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

唯一会困扰您的实际情况是,如果您正在使用git-bisect之类的东西在同一build目录中尝试较旧版本的代码。在这种情况下,您可能需要清理和编译多余的内容,以确保列表中包含正确的文件。这是一个极端的情况,而且您已经脚尖了,这并不是一个真正的问题。


1
遍历也很糟糕:git的difftool文件存储为$ basename。$ ext。$ type。$ pid。$ ext,这在尝试在单个合并解析后进行编译时会引起有趣的错误。
mathstuf

9
我认为这个答案掩盖了丢失新文件的缺点,Simply "touch" the CMakeLists.txt如果您是开发人员,那是可以的,但是对于其他构建您的软件的人来说,这确实是一个痛苦的痛点,因为您的构建在更新后会失败,并且负担了他们的调查工作为什么。
ideaman42

36
你知道吗?自6年前编写此答案以来,我已经改变了主意,现在更喜欢显式列出文件。唯一真正的缺点是“添加文件需要更多的工作”,但是它可以避免各种麻烦。在很多方面,显式胜于隐式。
richq

1
@richq 这个git hook是否会让您重新考虑当前位置?:)
Antonio

8
正如安东尼奥所说,投票是为了鼓吹“通俗”的方法。对于那些选民来说,改变答案的性质是一个诱饵和开关。作为一种折衷,我添加了一个编辑以反映我的意见更改。我为在茶杯中引起这样的风暴向互联网表示歉意:-P
richq '16

113

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

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

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

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

当然,您可能想知道不利之处-继续阅读!


抢球失败时:

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

如果您是添加文件的人,这似乎是可以接受的折衷方案,但这会给其他人构建代码带来麻烦,他们会从版本控制中更新项目,运行构建,然后与您联系,抱怨
“构建的破碎”。

更糟的是,该故障通常会给出一些链接错误,这些错误不会给问题的原因提供任何提示,并且会浪费时间进行故障排除。

在我从事的一个项目中,我们开始进行通配,但是在添加新文件时收到了很多投诉,因此有足够的理由明确列出文件而不是通配。

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

因此,我不推荐这样做,它所带来的问题远远超过了便利性,当有人因此而无法构建您的软件时,他们可能会浪费大量时间来查找问题或放弃。

另外要注意的是,仅记住触摸CMakeLists.txt并不总是足够的,对于使用globlob的自动构建,我必须cmake每次构建之前运行,因为自上次构建*之后可能已添加/删除了文件。

规则的例外:

在某些情况下,最好使用通配符:

  • 用于CMakeLists.txt为不使用CMake的现有项目设置文件。
    这是获取所有源代码的快速方法(一旦构建系统运行,请使用显式文件列表替换globlob)。
  • 当不使用CMake作为主要构建系统时,例如,如果您正在使用的项目没有使用CMake,而您想为其维护一个自己的构建系统。
  • 对于文件列表更改频繁而无法维护的任何情况。在这种情况下,它可能会很有用,但是您必须每次都接受运行cmake以生成生成文件才能获得可靠/正确的生成(这与CMake的意图背道而驰-将配置从生成中分离出来的能力)

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


9
“遍历的最大缺点是创建新文件不会自动更新构建系统。” 但是,如果您不熟悉,是否还必须手动更新CMakeLists.txt,这意味着cmake仍不会自动更新构建系统,这不是真的吗?似乎您必须记住手动执行某种操作才能构建新文件。触摸CMakeLists.txt似乎比打开它并对其进行编辑以添加新文件更容易。
2014年

17
@Dan,对于您的系统-当然,如果只开发一个就可以了,但是其他构建您的项目的人呢?您要给他们发送电子邮件并手动触摸CMake文件吗?每次添加或删除文件?-将文件列表存储在CMake中可确保生成的版本始终使用vcs知道的相同文件。相信我-这不仅是一些微妙的细节-当许多开发人员的构建失败时,他们会邮寄清单并在IRC上询问代码是否已损坏。注意:(即使在您自己的系统上,您也可能返回git历史记录,例如,并且不认为会进入并触摸CMake文件)
ideaman42 2014年

2
啊,我没有想到那件事。那是我听说过的最好的理由。我希望cmake文档扩展了为什么他们建议人们避免使用glob。
2014年

1
我一直在考虑将最后一次执行的时间戳写入文件的解决方案。唯一的问题是:1)它可能必须通过cmake才能实现跨平台,因此我们需要避免以某种方式再次运行cmake。2)可能会有更多的合并冲突(在文件列表btw中仍然会发生)在这种情况下,可以通过采用稍后的时间戳来轻松解决它们。
Predelnik '17

2
@ tim-mb,“但是,如果CMake创建了一个您可以检入的filetree_updated文件,它将在每次文件更新时自动更改,这将是很好的。” -您刚刚完全描述了我的回答。
Glen Knowles

21

在CMake 3.12中,file(GLOB ...)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})

但是,有些人仍然建议避免使用源代码。实际上,文档指出:

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

就个人而言,我认为不必手动管理源文件列表以克服可能的弊端所带来的好处。如果必须切换回手动列出的文件,则只需打印全局源列表并将其粘贴回即可轻松实现。


如果您的构建系统执行了完整的cmake和构建周期(删除构建目录,从那里运行cmake,然后调用makefile),前提是它们没有拉入不需要的文件,那么使用GLOBbed源肯定没有缺点吗?以我的经验,cmake的部分比构建的运行速度快得多,因此无论如何这并没有太多的开销
Den-Jason

9

您可以安全地(可能应该)以附加文件的形式来保存(包括)依赖关系。

在某处添加以下功能:

# 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)

然后遍历:

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

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

唯一的过程更改是在创建新文件之后。如果不进行全局设置,则工作流程是从Visual Studio内部修改CMakeLists.txt并进行重建,如果进行全局设置,则显式运行cmake-或仅触摸CMakeLists.txt。


起初我以为这是一个可以在添加源文件时自动更新Makefile的工具,但是现在我看到了它的价值。真好!这解决了有人从存储库更新并make给出奇怪的链接器错误的问题。
克里斯·伦戈

1
我相信这可能是个好方法。当然,仍然需要记住在添加或删除文件后触发cmake,并且还需要提交此依赖项文件,因此需要在用户方面进行一些培训。主要缺点可能是此依赖文件可能引发讨厌的合并冲突,如果不再次要求开发人员对该机制有所了解,则可能难以解决。
安东尼奥

1
如果您的项目有条件地包含了文件(例如,某些仅在启用功能时使用或仅用于特定操作系统的文件),则此方法将无效。对于便携式软件而言,足够普遍的是,某些文件仅用于特殊平台。
ideaman42 '18年

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.