最简单但完整的CMake示例

Arn*_*rne 110 c++ cmake project-setup

不知何故,我对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 ..]
Run Code Online (Sandbox Code Playgroud)

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

Arn*_*rne 85

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

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

理解cmake命令的基本规则是以下语法: <function-name>(<arg1> [<arg2> ...])without comma或semicolor.每个参数都是一个字符串.foobar(3.0)并且foobar("3.0")是一样的.你可以设置列表/变量set(args arg1 arg2).使用此变量集foobar(${args}) 并且foobar(arg1 arg2)实际上是相同的.不存在的变量等同于空列表.列表在内部只是一个字符串,用分号分隔元素.因此,只有一个元素的列表只是定义该元素,不会发生装箱.变量是全球性的.内置函数提供某种形式的命名参数,因为它们期望某些id像PUBLICor或者DESTINATION在他们的参数列表中,对参数进行分组.但这不是语言特性,那些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)
Run Code Online (Sandbox Code Playgroud)

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

sgv*_*gvd 38

最基本但完整的示例可以在cmake教程中找到:

cmake_minimum_required (VERSION 2.6)
project (Tutorial)
add_executable(Tutorial tutorial.cxx)
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

对于您的其他问题,本教程中还有一种方法:创建一个包含在代码中的可配置头文件.为此,请创建一个configuration.h.in包含以下内容的文件:

#define RESOURCES_PATH "@RESOURCES_PATH@"
Run Code Online (Sandbox Code Playgroud)

然后在你的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}")
Run Code Online (Sandbox Code Playgroud)

最后,您需要代码中的路径,您可以:

#include "configuration.h"

...

string resourcePath = string(RESOURCE_PATH) + "file.png";
Run Code Online (Sandbox Code Playgroud)

  • @sgvd`string resourcePath = string(RESOURCE_PATH)+"file.png"`恕我直言,将绝对**路径硬编码到源目录是一个坏主意.如果您需要安装项目怎么办? (3认同)
  • 我知道自动收集来源听起来不错,但它可能会导致各种各样的复杂情况.刚才看到这个问题进行简短的讨论:http://stackoverflow.com/q/10914607/1401351. (2认同)
  • 如果你不运行cmake,你会得到完全相同的错误; 手动添加文件需要一秒钟,每次编译运行cmake每次需要一秒钟; 你真的打破了cmake的一个特征; 一个人在同一个项目上工作并拉入你的更改会做:运行make - >获取未定义的引用 - >希望记得重新运行cmake,或者文件bug - >运行cmake - >运行make make,而如果你添加文件他用手做:成功运行 - >与家人共度时光.总而言之,不要懒惰,并且让自己和别人在将来头疼. (2认同)

sta*_*all 9

注意:这个答案很长,因为这个看似无辜的问题及其涵盖的多个主题看似广泛,而且该问题的目标受众是初学者(CMake 的新手)。相信我——这个答案可能比现在长得多。

CMake 是什么

不知何故,我对 CMake 的工作原理感到完全困惑。每当我认为我越来越接近理解 CMake 的编写方式时,它就会在我阅读的下一个示例中消失。

CMake 是一个构建系统生成器。您编写配置来描述构建系统(项目及其构建目标以及应如何构建它们)(以及可选的测试安装打包)。您为 CMake 程序提供该配置并告诉它要生成哪种构建系统,它会生成它(前提是支持该构建系统)。此类受支持的构建系统包括(但不限于):NinjaUnix MakefilesVisual Studio 解决方案XCode

构建系统是理解(因为你指示它)你的项目需要如何构建的东西——它有哪些源文件,这些源文件应该如何编译成目标文件,以及这些目标文件应该如何链接在一起可执行文件或动态/共享或静态库。

使用 CMake 的优点不应被低估。如果您想支持多个构建系统(这对于跨平台库作者来说尤其常见,他们希望允许用户做出自己的构建系统选择并节省这些用户编写这些构建系统配置的工作),那么需要做的工作要少得多编写一种 CMake 配置,而不是使用不同的构建系统用自己的语言和工作方式进行N配置。N

实际上,CMake 不仅支持C 和 C++,还支持其他编程语言及其构建系统,但由于您只是询问 C++,所以我将省略它。

使用 CMake 时避免使用 CMake 的技巧(孩子们,不要在家尝试)

我想知道的是,我应该如何构建我的项目,以便我的 CMake 将来需要最少的维护。例如,当我在 src 树中添加新文件夹时,我不想更新 CMakeList.txt,该文件夹的工作方式与所有其他 src 文件夹完全相同。

与您的想法相反,每次添加新源文件时都必须稍微修改 CMakeLists.txt 文件,这对于 CMake 可以提供的所有好处来说是非常小的成本,并且尝试规避该成本也有其自身的成本这会成为大规模的问题。这就是为什么CMake 的维护者以及 Stack Overflow 上 CMake 的各种长期用户file(GLOB)不鼓励使用人们经常使用的规避技术(即),如此处和此处所示

当您有一个包含几个文件的小项目时,在 CMakeLists.txt 文件中列出这几个源文件并不是很混乱,而当您有一个包含大量源文件的大项目时,您最好还是列出出于先前在链接资源中列出的原因,明确地使用源文件。简而言之,这是为了您自己的利益和理智。不要试图对抗它。

基本的 CMake 配置

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

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]
Run Code Online (Sandbox Code Playgroud)

如果您正在寻找用于项目文件系统布局的约定,那么Pitchfork 布局约定 (PFL)是一个明确指定的布局规范,它是根据 C++ 社区随着时间的推移出现的约定编写的。

以下是该项目布局的外观,遵循带有拆分标头拆分测试的PFL 规范:

myProject
    src/
        module1/
            module1.h
            module1.cpp
        module2/
            [...]
        main.cpp
    test/
        test1.cpp
    resources/
        file.png
    bin
        [execute cmake ..]
Run Code Online (Sandbox Code Playgroud)

注意:不一定是include/myProject_module1/- 它可以是include/,但是添加myProject_module1/会使#includes每个模块“命名空间”,以便两个模块(即使一个来自单独的项目)可以具有相同名称的头文件,并且这些标头可以全部包含在一个源文件中,而不会发生冲突或歧义,如下所示:

myProject/
  CMakeLists.txt
  libs/
    module1/
      CMakeLists.txt
      include/myProject_module1/
        module1.h
      src/myProject_module1/
        module1.cpp
      tests/
        CMakeLists.txt
        test1.cpp
      data/
        file.png
    module2/
      CMakeLists.txt
      src/module2
        main.cpp
      [...]
  build/
Run Code Online (Sandbox Code Playgroud)

既然您在问题中允许这样做,对于其余的配置代码示例,我将使用上面的 PFL 布局。

myProject/CMakeLists.txt:

cmake_minimum_required(VERSION 3.25)
# ^choose a CMake version to support (its own can of worms)
# see https://alexreinking.com/blog/how-to-use-cmake-without-the-agonizing-pain-part-1.html
project(example_project
  VERSION 0.1.0 # https://semver.org/spec/v0.1.0.html
  DESCRIPTION "a simple CMake example project"
  # HOMEPAGE_URL ""
  LANGUAGES CXX
)
if(EXAMPLE_PROJECT_BUILD_TESTING)
  enable_testing()
  # or alternatively, `include(CTest)`, if you want to use CDash
  # https://cmake.org/cmake/help/book/mastering-cmake/chapter/CDash.html
endif()
add_subdirectory(libs/module1)
add_subdirectory(libs/module2)
# ^I generally order these from lower to higher abstraction levels.
# Ex. if module1 uses module2, then add_subdirectory it _after_ module2.
# That allows doing target_link_libraries inside moduleN/CmakeLists.txt
# instead of here (although that's equally fine. a matter of preference).
Run Code Online (Sandbox Code Playgroud)

cmake_minimum_required()命令是您声明解析和运行 CMake 配置所需的 CMake 版本的位置。该project()命令是声明基本项目信息的地方,该enable_testing()命令启用对当前目录及以下目录的测试,每个add_subdirectory命令都会更改“当前目录”,创建一个新的子目录“范围”,并解析在该目录中找到的 CMakeLists.txt 文件小路。

cmake_minimum_required()以下是、project()enable_testing()的文档add_subdirectory()

myProject/libs/module1/CMakeLists.txt (与 module2 类似):

# if module1 is a library, use add_library() instead
add_executable(module1
  src/module1.cpp
)
target_compile_features(module1 PUBLIC cxx_std_20) # or whatever language standard you are using
target_include_directories(module1 PUBLIC include)
if(EXAMPLE_PROJECT_BUILD_TESTING)
  add_subdirectory(tests)
endif()
Run Code Online (Sandbox Code Playgroud)

add_executable()命令是您声明要构建的新可执行目标的方式。类似add_library,但是是如何声明要构建的库目标的,可以通过target_link_libraries()命令链接到该库。

target_compile_features()命令是您如何告诉 CMake 将什么标志传递给编译器以选择要使用的 C++ 语言标准的方式,该target_include_directories()命令是您如何告诉 CMake 在编译实现文件时要指定哪些包含目录。PUBLIC意味着目标本身需要包含其#includes 的目录,并且链接到该目标的其他依赖目标也将需要。如果依赖目标不需要它,请使用PRIVATE. 如果只有依赖目标需要它,请使用INTERFACE. PUBLICPRIVATE、 和 与INTERFACE库目标相关,但我不知道它们对可执行目标有任何用处(因为我很确定没有任何东西依赖于可执行文件的链接),因此在指定包含目录时,或者PUBLICPRIVATE应该起作用可执行目标。

add_library()以下是、add_executable()、和的文档。target_compile_features() target_include_directories()target_link_libraries()

如果您想了解有关任何 CMake 命令(在 中列出cmake --help-command-list)的更多信息,只需执行cmake --help <command>或 google即可cmake command <command>

就tests文件夹而言,您可以在不使用任何框架的C++测试库的情况下使用CMake的测试支持,或者与支持CMake的测试库或框架一起使用。要了解有关 CMake 和一般测试的更多信息,请阅读《掌握 CMake》一书中的章节。内容太多,无法以 Stack Overflow 答案的形式涵盖。

安装也是它自己的蠕虫罐头(稍后会详细介绍),所以既然问题没有要求它,我认为最好不要在答案中留下,以避免超长的帖子和范围蔓延。再次参见《掌握 CMake》一书中的专门章节。安装时要特别注意的一件事是确保安装包可重定位

就一个非常非常简单的项目而言,这就是您所需要的全部,当然,根据您项目的特定需求,可以进行更多的配置,但是当您到达那里时,您可能会在这些桥梁上烧毁自己。

编译选项/标志也是它自己的蠕虫病毒,最好在单独的问答帖子中进行介绍。

如果您想开始使用依赖项,我建议阅读官方的“使用依赖项”指南

如果您确实有很多源文件,并且您的 myProject/src/module1/CMakeLists.txt 文件由于add_executable/的所有行而开始变得笨重,那么您可以使用另一个 CmakeListsadd_library将其分解为一个单独的文件。target_sources()通过 包含的子目录中的 txt 文件add_subdirectory,或通过 包含的同一子目录中的sources.cmake 文件include()

要生成目录树图中所示的构建系统,请将当前目录更改为.../myProject然后运行cmake -S . -B build <...>​​,其中<...>您要使用的任何其他配置参数

与往常一样,CMake 参考文档可能非常有帮助,但在前几次查看时会感到不知所措,因为它们不适合初学者学习。如果您想了解有关如何使用 CMake 的更多信息,请尝试官方 CMake 教程,并阅读Mastering CMake 一书中的相关章节。如果您确实想深入了解,请查看“Professional CMake” - 由 Craig Scott(CMake 维护者之一)撰写。它需要花钱,但阅读了 Craig 的示例章节、目录以及 CMake GitLab 上的其他博客文章和提案后,我对其价值充满信心,而且本书的新版本不需要额外费用。

资源文件的技巧

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

此回复是假设您选择 CMake 来使用它来实现所有它的价值——跨平台、灵活的工具链构建,而不是每个人都使用 CMake(这很好)。

这是它自己的蠕虫。一个棘手的部分是构建文件夹和安装文件夹之间的文件系统放置比较。构建文件夹是构建二进制文件和其他成分(例如目标文件)的位置,安装文件夹是安装这些构建的二进制文件的任何位置。它们可以具有非常不同的文件系统结构,其中构建文件夹布局由 CMake 决定,并且默认情况下它会执行合理的操作,但安装文件夹中的布局很大程度上取决于希望如何配置它。这可能与 CMake 在构建文件夹中执行的操作不同,您可能希望能够从构建文件夹和安装文件夹运行和测试二进制文件。因此,您需要找到一种方法来支持您的二进制文件在“开发时”(当您从构建文件夹运行时)和安装后(当您或您的用户运行已安装的二进制文件)。

对于安装时放置资源文件的位置,不同平台上也有各种不同的约定。GNU 编码标准定义了一个约定,CMake 与 和 进行了一些集成/支持,但当然,Microsoft Windows 对 和 有另一件事\Program Files\\Users\...\AppData\MacOS 对应用程序包和 也有另一件事/library/Application Support/。事情可能并不像你想象的那么简单。我对此了解不多(而且我可能是错的),但在我看来,这个话题足够大,可以有自己的问题,或者在 Stack Overflow 上有几个自己的问题。

有关其他相关的 CMake 位,请参阅:

对于简单/天真的方法的缺点示例,sgvd 的答案适用于构建目录,但在项目安装到的任何地方都不起作用,即使它以某种方式在构建器的机器上工作,事实上它使用绝对路径使得它在分发到具有不同约定的其他机器或平台时不太可能工作。

如果您正在使用 CMake 但只想支持特定平台,那么您很幸运,可以在 Stack Overflow 上找到或写一个关于如何做到这一点的问题。

另请参阅此相关问题(在撰写本文时仍然没有答案):How to specify asset paths that work across builds

送行

欢迎来到 CMake 的世界!稍等片刻,直到您了解生成器表达式!然后你就会真正开始玩得开心!

^开玩笑地说,但没有开玩笑的生成器表达式可能非常有用,我会在任何一天选择它们,而不是在 Visual Studio 配置 UI 中受苦或手动编辑 Visual Studio 解决方案文件(仅举一个构建系统的示例)。