【问题标题】:Most simple but complete CMake example最简单但完整的 CMake 示例
【发布时间】:2026-01-26 14:55:01
【问题描述】:

不知何故,我对 CMake 的工作方式完全感到困惑。每次我认为我越来越接近理解 CMake 的编写方式时,它在我阅读的下一个示例中消失了。我想知道的是,我应该如何构建我的项目,以便我的 CMake 在未来需要最少的维护。例如,当我在我的 src 树中添加一个新文件夹时,我不想更新我的 CMakeList.txt,它的工作方式与所有其他 src 文件夹完全相同。

这就是我想象我的项目结构的方式,但这只是一个例子。如果推荐的方式不同,请告诉我,并告诉我该怎么做。

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]

顺便说一句,我的程序知道资源在哪里很重要。我想知道管理资源的推荐方式。我不想使用“../resources/file.png”访问我的资源

【问题讨论】:

  • For example I don't want to update my CMakeList.txt when I am adding a new folder in my src tree你能举个IDE自动收集源代码的例子吗?
  • no ide 通常不会自动收集资源,因为它们不需要。当我添加一个新文件或文件夹时,我会在 ide 中执行此操作,并且项目会更新。当我更改某些文件时,另一端的构建系统不会注意到,因此它会自动收集所有源文件是一种理想的行为

标签: c++ cmake project-setup


【解决方案1】:

经过一些研究,我现在有了自己版本的最简单但完整的 CMake 示例。在这里,它试图涵盖大部分基础知识,包括资源和包装。

它做的一件非标准的事情是资源处理。默认情况下,CMake 想要将它们放在 /usr/share/、/usr/local/share/ 和 Windows 上的等效项中。我想要一个简单的 zip/tar.gz,你可以在任何地方解压并运行。因此,资源是相对于可执行文件加载的。

理解 CMake 命令的基本规则是以下语法: <function-name>(<arg1> [<arg2> ...]) 不带逗号或分号。每个参数都是一个字符串。 foobar(3.0)foobar("3.0") 是一样的。您可以使用set(args arg1 arg2) 设置列表/变量。使用此变量集 foobar(${args})foobar(arg1 arg2) 实际上是相同的。不存在的变量相当于一个空列表。列表在内部只是一个用分号分隔元素的字符串。因此,只有一个元素的列表根据定义就是那个元素,不会发生装箱。变量是全局的。内置函数提供了某种形式的命名参数,因为它们希望在参数列表中使用一些像 PUBLICDESTINATION 这样的 id 来对参数进行分组。但这不是语言特性,那些 id 也只是字符串,由函数实现解析。

您可以从github 克隆所有内容

cmake_minimum_required(VERSION 3.0)
project(example_project)

###############################################################################
## file globbing ##############################################################
###############################################################################

# these instructions search the directory tree when cmake is
# invoked and put all files that match the pattern in the variables 
# `sources` and `data`
file(GLOB_RECURSE sources      src/main/*.cpp src/main/*.h)
file(GLOB_RECURSE sources_test src/test/*.cpp)
file(GLOB_RECURSE data resources/*)
# you can use set(sources src/main.cpp) etc if you don't want to
# use globing to find files automatically

###############################################################################
## target definitions #########################################################
###############################################################################

# add the data to the target, so it becomes visible in some IDE
add_executable(example ${sources} ${data})

# just for example add some compiler flags
target_compile_options(example PUBLIC -std=c++1y -Wall -Wfloat-conversion)

# this lets me include files relative to the root src dir with a <> pair
target_include_directories(example PUBLIC src/main)

# this copies all resource files in the build directory
# we need this, because we want to work with paths relative to the executable
file(COPY ${data} DESTINATION resources)

###############################################################################
## dependencies ###############################################################
###############################################################################

# this defines the variables Boost_LIBRARIES that contain all library names
# that we need to link to
find_package(Boost 1.36.0 COMPONENTS filesystem system REQUIRED)

target_link_libraries(example PUBLIC
  ${Boost_LIBRARIES}
  # here you can add any library dependencies
)

###############################################################################
## testing ####################################################################
###############################################################################

# this is for our testing framework
# we don't add REQUIRED because it's just for testing
find_package(GTest)

if(GTEST_FOUND)
  add_executable(unit_tests ${sources_test} ${sources})

  # we add this define to prevent collision with the main
  # this might be better solved by not adding the source with the main to the
  # testing target
  target_compile_definitions(unit_tests PUBLIC UNIT_TESTS)

  # this allows us to use our executable as a link library
  # therefore we can inherit all compiler options and library dependencies
  set_target_properties(example PROPERTIES ENABLE_EXPORTS on)

  target_link_libraries(unit_tests PUBLIC
    ${GTEST_BOTH_LIBRARIES}
    example
  )

  target_include_directories(unit_tests PUBLIC
    ${GTEST_INCLUDE_DIRS} # doesn't do anything on Linux
  )
endif()

###############################################################################
## packaging ##################################################################
###############################################################################

# all install commands get the same destination. this allows us to use paths
# relative to the executable.
install(TARGETS example DESTINATION example_destination)
# this is basically a repeat of the file copy instruction that copies the
# resources in the build directory, but here we tell cmake that we want it
# in the package
install(DIRECTORY resources DESTINATION example_destination)

# now comes everything we need, to create a package
# there are a lot more variables you can set, and some
# you need to set for some package types, but we want to
# be minimal here
set(CPACK_PACKAGE_NAME "MyExample")
set(CPACK_PACKAGE_VERSION "1.0.0")

# we don't want to split our program up into several things
set(CPACK_MONOLITHIC_INSTALL 1)

# This must be last
include(CPack)

【讨论】:

  • @SteveLorimer 我只是不同意,文件通配是一种不好的风格,我认为手动将文件树复制到 CMakeLists.txt 是一种不好的风格,因为它是多余的。但我知道人们确实不同意这个话题,因此我在代码中留下了评论,您可以在其中将 globbing 替换为明确包含所有源文件的列表。搜索set(sources src/main.cpp)
  • @SteveLorimer 是的,我经常不得不再次调用 cmake。每次我在目录树中添加一些东西时,我都需要手动重新调用 cmake,以便重新评估 globbing get。如果你把文件放在CMakeLists.txt,那么一个普通的make(或ninja)会触发cmake的重新调用,所以你不能忘记它。这也有点团队友好,因为这样团队成员也不能忘记执行 cmake。但我认为不应该仅仅因为有人添加了一个文件就需要触及一个makefile。写一次,以后再也不用考虑了。
  • @SteveLorimer 我也不同意在项目的每个目录中放置一个 CMakeLists.txt 的模式,它只是将项目的配置分散到各处,我认为一个文件就足够了,否则,您会失去对构建过程中实际完成的工作的概述。这并不意味着不能有自己的CMakeLists.txt的子目录,我只是认为它应该是一个例外。
  • 假设 "VCS""version control system" 的缩写,那么这无关紧要。问题不是,工件不会被添加到源代码控制中。问题是,CMake 将无法重新评估添加的源文件。它不会重新生成构建系统输入文件。构建系统会很高兴地坚持使用过时的输入文件,要么导致错误(如果你幸运的话),或者如果你运气不好,就会被忽视。 GLOBbing 在依赖计算链中产生了一个缺口。这一个重大问题,评论没有恰当地承认这一点。
  • CMake 和 VCS 完全隔离运行。 VCS 不知道 CMake,CMake 也不知道任何 VCS。它们之间没有联系。除非您建议开发人员应该采取手动步骤,从 VCS 中获取信息,并基于一些启发式清理并重新运行 CMake。显然,这无法扩展,并且容易受到人类特有的谬误的影响。不,抱歉,到目前为止,您还没有为 GLOBbing 文件提出令人信服的观点。
【解决方案2】:

最基本但最完整的例子可以在CMake tutorial找到:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)

对于您的项目示例,您可能有:

cmake_minimum_required (VERSION 2.6)
project (MyProject)
add_executable(myexec src/module1/module1.cpp src/module2/module2.cpp src/main.cpp)
add_executable(mytest test1.cpp)

对于您的其他问题,本教程再次介绍了一种方法:创建一个包含在代码中的可配置头文件。为此,创建一个文件configuration.h.in,内容如下:

#define RESOURCES_PATH "@RESOURCES_PATH@"

然后在你的CMakeLists.txt 添加:

set(RESOURCES_PATH "${PROJECT_SOURCE_DIR}/resources/"
# configure a header file to pass some of the CMake settings
# to the source code
configure_file (
  "${PROJECT_SOURCE_DIR}/configuration.h.in"
  "${PROJECT_BINARY_DIR}/configuration.h"
)

# add the binary tree to the search path for include files
# so that we will find TutorialConfig.h
include_directories("${PROJECT_BINARY_DIR}")

最后,如果你需要代码中的路径,你可以这样做:

#include "configuration.h"

...

string resourcePath = string(RESOURCE_PATH) + "file.png";

【讨论】:

  • 非常感谢,特别是对于 RESOURCE_PATH,不知怎的,我不知道 configure_file 是我要找的。但是您手动添加了项目中的所有文件,有没有更好的方法来简单地定义从 src 树中添加所有文件的模式?
  • 请参阅 Dieter 的回答,以及我的 cmets 了解您为什么不应该使用它的原因。如果您真的想自动化它,更好的方法可能是编写一个脚本,您可以运行该脚本来重新生成源文件列表(或使用 cmake 感知 IDE 为您执行此操作;我不熟悉)。
  • @sgvd string resourcePath = string(RESOURCE_PATH) + "file.png" 恕我直言,将 absolute 路径硬编码到源目录是个坏主意。如果您需要安装项目怎么办?
  • 我知道自动收集资源听起来不错,但它会导致各种复杂情况。请参阅前段时间的这个问题进行简短讨论:*.com/q/10914607/1401351
  • 如果你不运行 cmake,你会得到完全相同的错误;手动添加文件一次需要一秒钟,每次编译运行 cmake 每次需要一秒钟;你实际上破坏了cmake的一个特性;在同一个项目上工作并提取您的更改的人会这样做:运行 make -> 获取未定义的引用 -> 希望记得重新运行 cmake,或者文件错误与您 -> 运行 cmake -> 成功运行 make,而如果您添加文件他亲自动手:成功运行 -> 与家人共度时光。总结一下,不要偷懒,以后免得自己和别人头疼。
【解决方案3】:

这里我写了一个最简单但完整的 CMakeLists.txt 文件示例。

Source Code

  1. 从 hello world 到跨平台 Android/iOS/Web/Desktop 的教程。
  2. 每个平台我都发布了一个示例应用程序。
  3. 08-cross_platform 文件结构已通过我的工作验证
  4. 对于我自己的团队来说,这可能并不完美,但很有用和最佳实践

之后,我提供了详细的文档。

如果您有任何问题,可以联系我,我愿意解释。

【讨论】: