cmake学习
先修:Makefile
好用的cmake学习资料:
现代cmake
重要概念:
Project:
project()
命令用于定义一个项目的名称和版本,它设定了项目的上下文,并允许CMake进行版本检查等。项目是所有构建目标(如库、程序等)的容器。Target:
target
代表了构建过程中的一个输出实体,如可执行文件、库文件、模块等。library
和program
是target
的两种常见类型。一个项目可以包含多个目标。add_executable()
,add_library()
,add_custom_target()
等命令可添加目标。Program (可执行文件):
程序通常是指通过add_executable()
命令创建的可执行文件目标。它是由源代码文件编译并链接而成的,可以直接在操作系统上运行。Library (库):
库是通过add_library()
命令创建的,它可以是静态库(.a, .lib)或动态库(.so, .dll)。库包含可以被其他程序或库使用的代码和数据。Source (源代码):
源代码是指用编程语言编写的文本文件,这些文件包含程序或库的指令。在CMake中,源代码文件通常被指定为add_executable()
或add_library()
等命令的参数。CMake负责编译这些源代码文件来生成目标(如程序或库)。Package:
它指的是一个可安装和可重用的软件组件,该组件可能包含库、可执行文件、头文件、配置文件等。CMake通过install()
命令支持将构建的目标(如库和程序)以及其他文件安装到指定的目录结构中,从而创建可分发的软件包。
一些反复出现的命令:
1 | include() # 直接插入另一个文件内容 |
根目录中的CMakeLists.txt
文件
cmake_minimum_required(VERSION x.y.z)
:指定CMake的最低版本要求。project(MyProject VERSION x.y.z)
:定义项目的名称和版本。find_package()
:查找并加载外部项目(如依赖库)的配置文件。add_subdirectory()
:添加子目录,CMake将递归地处理这些子目录中的CMakeLists.txt
文件。- (可选)
set()
或option()
:设置全局变量或选项。
子目录(如src
)中的CMakeLists.txt
文件
子目录中的CMakeLists.txt
文件通常负责定义该子目录下的目标(如源文件、库、可执行文件)以及它们之间的依赖关系。
add_executable()
或add_library()
:定义可执行文件或库目标,并列出该目标的源文件。target_sources()
:向已定义的目标添加额外的源文件。target_include_directories()
:为目标指定包含目录,以便编译器能够找到这些目录下的头文件。target_compile_definitions()
:为目标指定编译定义。target_compile_options()
:为目标指定编译选项。target_link_libraries()
:指定目标应该链接哪些库。- (可选)
include()
:包含其他CMake文件,以重用配置或函数。
示例
**项目根目录的CMakeLists.txt
**:
1 | cmake_minimum_required(VERSION 3.10) |
**src
目录下的CMakeLists.txt
**:
1 | # 定义库 |
在这个例子中,根目录的CMakeLists.txt
负责设置CMake版本、项目名称和版本,查找依赖库,并添加src
子目录。而src
目录下的CMakeLists.txt
则定义了库MyLib
和可执行文件MyApp
,并指定了它们之间的依赖关系以及包含目录。
命令行调用
1 | cmake -B build -DCMAKE_BUILD_TYPE=Release |
命令行技巧
-D 选项:指定配置变量(又称缓存变量)
CMake 项目的构建分为两步:
cmake -B build
配置阶段(configure),检测环境并生成构建规则,在 build 目录下生成本地构建系统能识别的项目文件(Makefile 或 .sln)cmake --build build
,构建阶段(build),调用编译器来编译代码
在配置阶段可以通过 -D 设置缓存变量。
第二次配置时,之前的 -D 添加仍然会被保留。
cmake -B build -DCMAKE_INSTALL_PREFIX=/opt/openvdb-8.0
设置安装路径为 /opt/openvdb-8.0(会安装到 /opt/openvdb-8.0/lib/libopenvdb.so)cmake -B build -DCMAKE_BUILD_TYPE=Release
设置构建模式为发布模式(开启全部优化)cmake -B build
第二次配置时没有 -D 参数,但是之前的 -D 设置的变量都会被保留
(此时缓存里仍有你之前定义的 CMAKE_BUILD_TYPE 和 CMAKE_INSTALL_PREFIX)
-G 选项:指定要用的生成器
- Linux 系统上的 CMake 默认用是 Unix Makefiles 生成器;Windows 系统默认是 Visual Studio 2019 生成器;MacOS 系统默认是 Xcode 生成器。
- 可以用 -G 参数改用别的生成器,例如
cmake -GNinja
会生成 Ninja 这个构建系统的构建规则。Ninja 是一个高性能,跨平台的构建系统,Linux、Windows、MacOS 上都可以用。 - 而 Ninja 则是专为性能优化的构建系统,和 CMake 结合是行业标准。
- 性能上:Ninja > Makefile > MSBuild
添加源文件
当源码在同一目录下的多文件中:
使用 GLOB 自动查找当前目录下指定扩展名的文件,实现批量添加源文件
启用 CONFIGURE_DEPENDS 选项,当添加新文件时,自动更新变量
1 | add_executable(main) |
当源码在子文件中:
1 | add_executable(main) |
1 | add_executable(main) |
GLOB_RECURSE 的问题:会把 build 目录里生成的临时 .cpp 文件也加进来
解决方案:把源码统一放到 src 目录下
项目配置变量
CMAKE_BUILD_TYPE 是 CMake 中一个特殊的变量,用于控制构建类型,他的值可以是:
- Debug 调试模式,完全不优化,生成调试信息,方便调试程序
- Release 发布模式,优化程度最高,性能最佳,但是编译比 Debug 慢
- MinSizeRel 最小体积发布,生成的文件比 Release 更小,不完全优化,减少二进制体积
- RelWithDebInfo 带调试信息发布,生成的文件比 Release 更大,因为带有调试的符号信息
- 默认情况下 CMAKE_BUILD_TYPE 为空字符串,这时相当于 Debug。
标准模板:
1 | if (NOT CMAKE_BUILD_TYPE) |
链接库文件
main.cpp 调用 mylib.cpp 里的 say_hello 函数
mylib 作为一个静态库
1 | add_library(mylib STATIC mylib.cpp) |
mylib 作为一个动态库
1 | add_library(mylib SHARED mylib.cpp) |
mylib 作为一个对象库
1 | add_library(mylib OBJECT mylib.cpp) |
- 对象库类似于静态库,但不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件
- 对象库是 CMake 自创的,绕开了编译器和操作系统的各种繁琐规则,保证了跨平台统一性。
- 在自己的项目中,推荐全部用对象库(OBJECT)替代静态库(STATIC)避免跨平台的麻烦。
- 对象库仅仅作为组织代码的方式,而实际生成的可执行文件只有一个,减轻了部署的困难。
- 对象库可以绕开编译器的不统一:保证不会自动剔除没引用到的对象文件
- 虽然动态库也可以避免剔除没引用的对象文件,但引入了运行时链接的麻烦
设置对象属性的三种方式
set_property
设置单个属性
1 | add_executable(main main.cpp) |
set_target_properties
设置多个属性
1 | # CMakeLists.txt |
set
通过全局变量,让之后创建的所有对象都享有同样的属性
要注意此时 set(CMAKE_xxx) 必须在 add_executable 之前才有效。
1 | cmake_minimum_required(VERSION 3.x) # 注意:这里假设需要CMake的某个3.x版本,但具体版本号需要您根据需求填写 |
Windows 链接 dll 找不到
- 这是因为你的 dll 和 exe 不在同一目录。Windows 比较蠢,他只会找当前 exe 所在目录,然后查找 PATH,找不到就报错。而你的 dll 在其他目录,因此 Windows 会找不到 dll。
- 解决1:把 dll 所在位置加到你的 PATH 环境变量里去,一劳永逸。
- 解决2:把这个 dll,以及这个 dll 所依赖的其他 dll,全部拷贝到和 exe 文件同一目录下。
解决1:设置 mylib 对象的 xx_OUTPUT_DIRECTORY 系列属性
1 | add_library(mylib SHARED mylib.cpp mylib.h) |
链接第三方库
find_package
find_package(TBB REQUIRED)
和find_package(TBB CONFIG REQUIRED)
区别
- find_package(TBB REQUIRED) 会查找 /usr/lib/cmake/TBB/TBBConfig.cmake 这个配置文件,并根据里面的配置信息创建 TBB::tbb 这个伪对象(他实际指向真正的 tbb 库文件路径 /usr/lib/libtbb.so),之后通过 target_link_libraries 链接 TBB::tbb 就可以正常工作了。
- 其实更好的是通过 find_package(TBB CONFIG REQUIRED),添加了一个 CONFIG 选项。这样他会优先查找 TBBConfig.cmake(系统自带的)而不是 FindTBB.cmake(项目作者常把他塞在 cmake/ 目录里并添加到 CMAKE_MODULE_PATH)。这样能保证寻找包的这个 .cmake 脚本是和系统自带的 tbb 版本是适配的,而不是项目作者当年下载的那个版本的 .cmake 脚本。
- 当然,如果你坚持要用
find_package(TBB REQUIRED)
也是可以的。 - 没有 CONFIG 选项:先找 FindTBB.cmake,再找 TBBConfig.cmake,找不到则报错
- 有 CONFIG 选项:只会找 TBBConfig.cmake,找不到则报错
此外,一些老年项目(例如 OpenVDB)只提供 Find 而没有 Config 文件,这时候就必须用find_package(OpenVDB REQUIRED)
而不能带 CONFIG 选项。
组件
find_package
生成的伪对象(imported target)都按照“包名::组件名”的格式命名。
你可以在 find_package
中通过 COMPONENTS
选项,后面跟随一个列表表示需要用的组件。
示例:
1 | find_package(Qt5 COMPONENTS Widgets Gui REQUIRED) |
输出与变量
message(STATUS “…”) 表示信息类型是状态信息,有 – 前缀
message(WARNING “…”) 表示是警告信息
message(AUTHOR_WARNING “…”) 表示是仅仅给项目作者看的警告信息
AUTHOR_WARNING 的不同之处:可以通过 -Wno-dev 关闭
message(FATAL_ERROR “…”) 表示是错误信息,会终止 CMake 的运行
message(SEND_ERROR “…”) 表示是错误信息,但之后的语句仍继续执行
变量与缓存
清除缓存,其实只需删除 build/CMakeCache.txt 就可以了
设置缓存变量
语法是:set(变量名 “变量值” CACHE 变量类型 “注释”)
更新缓存变量的正确方法,是通过命令行参数:cmake -B build -Dmyvar=world
缓存变量更新:删 build
set 可以在后面加一个 FORCE 选项,表示不论缓存是否存在,都强制更新缓存。
不过这样会导致没办法用 -Dmyvar=othervalue 来更新缓存变量。