文章目录
- 1.为什么要学习现代 CMake?
- 2.命令行小技巧
- (1)传统的 CMake 软件构建/安装方式
- (2)现代 CMake 提供了更方便的 -B 和 --build 指令,不同平台,统一命令!
- (3)-D 选项:指定配置变量(又称缓存变量)
- (4)-G 选项:指定要用的生成器
- (5)-E: CMake命令行模式。
- 3.添加源文件
- (1)方式1:一个 .cpp 源文件用于测试
- (2)方式2:先创建目标,稍后再添加源文件
- (3)使用变量来存储
- (4)如果源码放在子文件夹里怎么办?
- 4.项目配置变量
- (1)CMAKE_BUILD_TYPE 构建的类型,调试模式还是发布模式
- (2)各种构建模式在编译器选项上的区别
- (3)project:初始化项目信息,当前 CMakeLists.txt 所在位置作为根目录。
- (4)PROJECT_x_DIR 和 CMAKE_CURRENT_x_DIR 的区别
- (5)子模块里也可以用 project 命令,将当前目录作为一个独立的子项目
- (6)project 的初始化:LANGUAGES 字段
- (7)设置 C++ 标准:CMAKE_CXX_STANDARD 变量
- (8)project 的初始化:VERSION 字段
- (9)CMake常见变量——Project和CMake相关信息
- (10)一个标准的 CMakeLists.txt 模板
- 5.链接库文件
- (1)改进方法1:mylib 作为一个静态库
- (2)改进方法2:mylib 作为一个动态库
- (3)改进方法3:mylib 作为一个对象库
- (4)静态库,对象库,动态库在自动剔除没有引用符号对象上的区别
- (5)add_library 无参数时,是静态库还是动态库?
- (6)常见坑点:动态库无法链接静态库
- 6.对象的属性
- (1)set_property
- (2)set_target_properties 批量设置多个属性
- (3)通过全局的变量,让之后创建的所有对象都享有同样的属性
- (4)不要通过target_compile_options设置C++标准
- (5)windows使用动态链接库
- 7.链接第三方库
- (1)find_package
- (2)find_package(TBB REQUIRED) 和find_package(TBB CONFIG REQUIRED) 区别?
- (3)find_package(Qt5 REQUIRED) 出错了
- 8.输出与变量
- (1)message
- (2)message 可以用于打印变量
- 8.变量与缓存
- (1)重复执行 cmake -B build 会有什么区别?
- (2)如何清除缓存
- (3)find_package 就用到了缓存机制
- (4)设置缓存变量
- (4)缓存变量的更新方法
- (5)缓存变量的类型
- 9.跨平台与编译器
- (1)在 CMake 中给 .cpp 定义一个宏
- (2)CMake 提供的一些简写变量
- (3)生成器表达式,简化成一条指令
- (4)判断当前用的是哪一款 C++ 编译器
- (5)从命令行参数指定编译器CMAKE_CXX_COMPILER
- (6)vimrc
- 10.分支与判断
- (1)BOOL 类型的值
- (2)if 的特点
- 11.变量与作用域
- (1)变量的传播规则:父会传给子
- (2)变量的传播规则:子不传给父
- (3)子模块需要向父模块里传变量
- (4)除了父子模块之外还有哪些是带独立作用域的
- (5)环境变量的访问方式:$ENV{xx}
- (6)缓存变量的访问方式:$CACHE{xx}
- (7)if (DEFINED xx) 判断某变量是否存在
- 12.其他小建议
- (1)CCache:编译加速缓存
1.为什么要学习现代 CMake?
古代Cmake与现代Cmake
- 现代 CMake 指的是 CMake 3.x。
- 古代 CMake 指的是 CMake 2.x。
- 通过互联网和学校课程,许多人认识的 CMake 都是古代 CMake。
- 现代 CMake 和古代 CMake 相比,使用更方便,功能更强大。
- eg:my_course/course/11/01_source/00/CMakeLists.txt,类似
现代target_link_libraries的Public的应用:会自动将头文件、定义、其他库传播给你,
2.命令行小技巧
(1)传统的 CMake 软件构建/安装方式
#需要先创建 build 目录
mkdir build
##切换到 build 目录
cd build
##在 build 目录运行 cmake <源码目录> 生成 Makefile
cmake ..
##执行本地的构建系统 make 真正开始构建(4进程并行)
make -j4
#让本地的构建系统执行安装步骤
sudo make install
#回到源码目录
cd ..
(2)现代 CMake 提供了更方便的 -B 和 --build 指令,不同平台,统一命令!
#在源码目录用 -B 直接创建 build 目录并生成 build/Makefile
cmake -B build
#等于cmake --build build ---parallel 4
cmake --build build -j4
#sudo cmake --build build --target install
cmake -B build 免去了先创建 build 目录再切换进去再指定源码目录的麻烦。
cmake --build build 统一了不同平台(Linux 上会调用 make,Windows 上调用 devenv.exe)
- 结论:从现在开始,如果在命令行操作 cmake,请使用更方便的 -B 和 --build 命令。
(3)-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)
(4)-G 选项:指定要用的生成器
CMake 是一个跨平台的构建系统,可以从 CMakeLists.txt 生成不同类型的构建系统(比如 Linux 的 make,Windows 的 MSBuild),从而让构建规则可以只写一份,跨平台使用。
- 过去的软件(例如 TBB)要跨平台,只好 Makefile 的构建规则写一份,MSBuild 也写一份。
- 现在只需要写一次 CMakeLists.txt,他会视不同的操作系统,生成不同构建系统的规则文件。
- 和操作系统绑定的构建系统(make、MSBuild)称为本地构建系统(native buildsystem)。
- 负责从 CMakeLists.txt 生成本地构建系统构建规则文件的,称为生成器(generator)。
- eg:
Linux 系统上的 CMake 默认用是 Unix Makefiles 生成器;
Windows 系统默认是 Visual Studio 2019 生成器;
MacOS 系统默认是 Xcode 生成器。
可以用 -G 参数改用别的生成器,
- 例如 cmake -GNinja 会生成 Ninja 这个构建系统的构建规则。Ninja 是一个高性能,跨平台的构建系统,Linux、Windows、MacOS 上都可以用。
- Ninja 可以从包管理器里安装,没有包管理器的 Windows 可以用 Python 的包管理器安装:
pip install ninja
CMake 也可以通过 pip install cmake 安装
- 事实上,MSBuild 是单核心的构建系统,Makefile 虽然多核心但因历史兼容原因效率一般。
而 Ninja 则是专为性能优化的构建系统,他和 CMake 结合都是行业标准了。 - eg:
Ninja 和 Makefile 简单的对比
- 性能上:Ninja > Makefile > MSBuild
Makefile 启动时会把每个文件都检测一遍,浪费很多时间。 - 特别是有很多文件,但是实际需要构建的只有一小部分,从而是 I/O Bound 的时候,Ninja 的速度提升就很明显。
- 然而某些专利公司的 CUDA toolkit 在 Windows 上只允许用 MSBuild 构建,不能用 Ninja
- eg:
(5)-E: CMake命令行模式。
为了真正做到与平台无关,CMake提供了一系列可以用于所有系统上的的命令。
- 以-E参数运行CMake会帮助你获得这些命令的用法。
- 可以使用的命令有:
chdir, copy, copy_if_different copy_directory, compare_files, echo, echo_append, environment, make_directory, md5sum, remove_directory, remove, tar, time, touch, touch_nocreate, write_regv, delete_regv, comspec, create_symlink。
在CMakeLists.txt中使用
- ${CMAKE_COMMAND} 代表cmake的全路径
add_custom_command(
OUTPUT
${wrap_BLAS_LAPACK_sources}
COMMAND
${CMAKE_COMMAND} -E tar xzf ${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
COMMAND
${CMAKE_COMMAND} -E touch ${wrap_BLAS_LAPACK_sources}
WORKING_DIRECTORY
${CMAKE_CURRENT_BINARY_DIR}
DEPENDS
${CMAKE_CURRENT_SOURCE_DIR}/wrap_BLAS_LAPACK.tar.gz
COMMENT
"Unpacking C++ wrappers for BLAS/LAPACK"
VERBATIM
)
- 参考:SirDigit
3.添加源文件
(1)方式1:一个 .cpp 源文件用于测试
- eg:my_course/course/11/02_project/01/main.cpp
- CMake 中添加一个可执行文件作为构建目标
(2)方式2:先创建目标,稍后再添加源文件
- eg:my_course/course/11/01_source/02/CMakeLists.txt
- 如果有多个源文件,
逐个添加即可,my_course/course/11/01_source/03/CMakeLists.txt
(3)使用变量来存储
- 建议把头文件也加上,这样在 VS 里可以出现在“Header Files”一栏,my_course/course/11/01_source/04/CMakeLists.txt,my_course/course/11/01_source/05/CMakeLists.txt
- 使用 GLOB 自动查找当前目录下指定扩展名的文件,实现批量添加源文件,my_course/course/11/01_source/07/CMakeLists.txt
启用 CONFIGURE_DEPENDS 选项,当添加新文件时,自动更新变量,my_course/course/11/01_source/08/CMakeLists.txt,my_course/course/11/01_source/08/CMakeLists.txt
(4)如果源码放在子文件夹里怎么办?
可以用aux_source_directory,自动搜集需要的文件后缀名,my_course/course/11/01_source/10/CMakeLists.txt
GLOB_RECURSE,能自动包含所有子文件夹下的文件,my_course/course/11/01_source/09/CMakeLists.txt
- GLOB_RECURSE 的问题:会把 build 目录里生成的临时 .cpp 文件也加进来
解决方案:要么把源码统一放到 src 目录下,要么要求使用者不要把 build 放到和源码同一个目录里,建议是把源码放到 src 目录下。
报错如下:
4.项目配置变量
(1)CMAKE_BUILD_TYPE 构建的类型,调试模式还是发布模式
CMAKE_BUILD_TYPE 是 CMake 中一个特殊的变量,用于控制构建类型,他的值可以是:
Debug 调试模式,完全不优化,生成调试信息,方便调试程序
Release 发布模式,优化程度最高,性能最佳,但是编译比 Debug 慢
MinSizeRel 最小体积发布,生成的文件比 Release 更小,不完全优化,减少二进制体积
RelWithDebInfo 带调试信息发布,生成的文件比 Release 更大,因为带有调试的符号信息
- 默认情况下 CMAKE_BUILD_TYPE 为空字符串,这时相当于 Debug。
- eg:my_course/course/11/02_project/01/CMakeLists.txt
(2)各种构建模式在编译器选项上的区别
- 在Release模式下,追求的是程序的最佳性能表现,在此情况下,编译器会对程序做最大的代码优化以达到最快运行速度。另一方面,由于代码优化后不与源代码一致,此模式下一般会丢失大量的调试信息。
- 此外,注意定义了 NDEBUG 宏会使 assert 被去除掉。
1. Debug: `-O0 -g`
2. Release: `-O3 -DNDEBUG`
3. MinSizeRel: `-Os -DNDEBUG`
4. RelWithDebInfo: `-O2 -g -DNDEBUG`
技巧:设定一个变量的默认值
- 如何让 CMAKE_BUILD_TYPE 在用户没有指定的时候为 Release,指定的时候保持用户指定的值不变呢。
就是说 CMake 默认情况下 CMAKE_BUILD_TYPE 是一个空字符串。
因此这里通过 if (NOT CMAKE_BUILD_TYPE) 判断是否为空,如果空则自动设为 Release 模式。 - 大多数 CMakeLists.txt 的开头都会有这样三行,为的是让默认的构建类型为发布模式(高度优化)而不是默认的调试模式(不会优化)。
绝大多数 CMakeLists.txt 开头都会有的部分,可以说是“标准模板”了。
(3)project:初始化项目信息,当前 CMakeLists.txt 所在位置作为根目录。
- 这里初始化了一个名称为 hellocmake 的项目,为什么需要项目名?
对于 MSVC,他会在 build 里生成 hellocmake.sln 作为“IDE 眼中的项目”
CMAKE_CURRENT_SOURCE_DIR 表示当前源码目录的位置,例如 ~/hellocmake。
CMAKE_CURRENT_BINARY_DIR 表示当前输出目录的位置,例如 ~/hellocmake/build。 - eg:my_course/course/11/02_project/02/CMakeLists.txt
(4)PROJECT_x_DIR 和 CMAKE_CURRENT_x_DIR 的区别
和子模块的关系:PROJECT_x_DIR 和 CMAKE_CURRENT_x_DIR 的区别,区别在于和子模块之间的关系
PROJECT_SOURCE_DIR 表示最近一次调用 project 的 CMakeLists.txt 所在的源码目录。
CMAKE_CURRENT_SOURCE_DIR 表示当前 CMakeLists.txt 所在的源码目录。
CMAKE_SOURCE_DIR 表示最为外层 CMakeLists.txt 的源码根目录。
利用 PROJECT_SOURCE_DIR 可以实现从子模块里直接获得项目最外层目录的路径。
不建议用 CMAKE_SOURCE_DIR,那样会让你的项目无法被人作为子模块使用。(最最最外层的项目目录)
- eg:my_course/course/11/02_project/02/mylib/CMakeLists.txt
目录结构
jiwangreal@ubuntu:~/code/my_course/course/11/02_project/02$ tree -d
.
├── build
│ └── mylib
│ └── CMakeFiles
└── mylib
11/02_project/02/CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(hellocmake)
message("PROJECT_NAME: ${PROJECT_NAME}")
message("PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message("CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message("PROJECT_IS_TOP_LEVEL:" ${PROJECT_IS_TOP_LEVEL})
add_executable(main main.cpp)
add_subdirectory(mylib)
11/02_project/02/mylib/CMakeLists.txt
project(subhellocmake)
message("mylib got PROJECT_NAME: ${PROJECT_NAME}")
message("mylib got PROJECT_SOURCE_DIR: ${PROJECT_SOURCE_DIR}")
message("mylib got PROJECT_BINARY_DIR: ${PROJECT_BINARY_DIR}")
message("mylib got CMAKE_CURRENT_SOURCE_DIR: ${CMAKE_CURRENT_SOURCE_DIR}")
message("mylib got CMAKE_CURRENT_BINARY_DIR: ${CMAKE_CURRENT_BINARY_DIR}")
message("mylib got PROJECT_IS_TOP_LEVEL:" ${PROJECT_IS_TOP_LEVEL})
- 测试:
其他相关变量
- 详见:链接
PROJECT_SOURCE_DIR:当前项目源码路径(存放main.cpp的地方)
PROJECT_BINARY_DIR:当前项目输出路径(存放main.exe的地方)
CMAKE_SOURCE_DIR:根项目源码路径(存放main.cpp的地方)
CMAKE_BINARY_DIR:根项目输出路径(存放main.exe的地方)
PROJECT_IS_TOP_LEVEL:BOOL类型,表示当前项目是否是(最顶层的)根项目。判断自己是否被当成一个子模块
PROJECT_NAME:当前项目名
CMAKE_PROJECT_NAME:根项目的项目名
(5)子模块里也可以用 project 命令,将当前目录作为一个独立的子项目
这样一来 PROJECT_SOURCE_DIR 就会是子模块的源码目录而不是外层了。
- 这时候 CMake 会认为这个子模块是个独立的项目,会额外做一些初始化。
- 他的构建目录 PROJECT_BINARY_DIR 也会变成 build/<源码相对路径>。
- 这样在 MSVC 上也会看见 build/mylib/mylib.vcxproj 的生成
(6)project 的初始化:LANGUAGES 字段
project(项目名 LANGUAGES 使用的语言列表…) 指定了该项目使用了哪些编程语言。
- 目前支持的语言包括:
C:C语言
CXX:C++语言
ASM:汇编语言
Fortran:老年人的编程语言
CUDA:英伟达的 CUDA(3.8 版本新增)
OBJC:苹果的 Objective-C(3.16 版本新增)
OBJCXX:苹果的 Objective-C++(3.16 版本新增)
ISPC:一种因特尔的自动 SIMD 编程语言(3.18 版本新增)
如果不指定 LANGUAGES,默认为 C 和 CXX。
- 常见问题:LANGUAGES 中没有启用 C 语言,但是却用到了 C 语言,eg:my_course/course/11/02_project/04/CMakeLists.txt
- 解决:方法1:改成 project(项目名 LANGUAGES C CXX) 即可
- 解决:方法2:也可以先设置 LANGUAGES NONE,之后再调用 enable_language(CXX),这样可以把 enable_language 放到 if 语句里,从而只有某些选项开启才启用某语言之类的、
my_course/course/11/02_project/05/CMakeLists.txt
(7)设置 C++ 标准:CMAKE_CXX_STANDARD 变量
CMAKE_CXX_STANDARD 是一个整数,表示要用的 C++ 标准。
- 比如需要 C++17 那就设为 17,需要 C++23 就设为 23。
- CMAKE_CXX_STANDARD_REQUIRED 是 BOOL 类型,可以为 ON 或 OFF,默认 OFF。
他表示是否一定要支持你指定的 C++ 标准:如果为 OFF 则 CMake 检测到编译器不支持 C++17 时不报错,而是默默调低到 C++14 给你用;
为 ON 则发现不支持报错,更安全。 - 参考:Enabling C++11 And Later In CMake
- CMAKE_CXX_EXTENSIONS 也是 BOOL 类型,默认为 ON。
设为 ON 表示启用 GCC 特有的一些扩展功能;OFF 则关闭 GCC 的扩展功能,只使用标准的 C++。
g++ -std=c++17,
g++ -std=gun++17,gcc夹带了一些私货,若只用gcc的话,并使用其特性,则可以设置为ON
- 要兼容其他编译器(如 MSVC)的项目,都会设为 OFF 防止不小心用了 GCC 才有的特性。
- 此外,最好是在 project 指令前设置 CMAKE_CXX_STANDARD 这一系列变量,这样 CMake 可以在 project 函数里对编译器进行一些检测,看看他能不能支持 C++17 的特性。
eg:my_course/course/11/02_project/06/CMakeLists.txt
常见误区:手动添加 -std=c++17 行不行?
- 请勿直接修改 CMAKE_CXX_FLAGS 来添加 -std=c++17(你在百度 CSDN 学到的用法)。
请使用 CMake 帮你封装好的 CMAKE_CXX_STANDARD(从业人员告诉你的正确用法)。 - 为什么百度不对:你 GCC 用户手动指定了 -std=c++17(gcc可以使用),让 MSVC 的用户怎么办?
此外 CMake 已经自动根据 CMAKE_CXX_STANDARD 的默认值 11 添加 -std=c++11 选项了,你再添加个 -std=c++17 选项不就冲突了吗?
所以请用 CMAKE_CXX_STANDARD。
(8)project 的初始化:VERSION 字段
project(项目名 VERSION x.y.z) 可以把当前项目的版本号设定为 x.y.z。
- 之后可以通过 PROJECT_VERSION 来获取当前项目的版本号。
PROJECT_VERSION_MAJOR 获取 x(主版本号)。
PROJECT_VERSION_MINOR 获取 y(次版本号)。
PROJECT_VERSION_PATCH 获取 z(补丁版本号)。
- eg:my_course/course/11/02_project/07/CMakeLists.txt
其他字段: - eg:my_course/course/11/02_project/08/CMakeLists.txt
项目名的另一大作用:
- 会设置另外 <项目名>_SOURCE_DIR 等变量
- 如果有很多项目project,则可以用类似hellocmake_SOURCE_DIR等这样的,在当前目录下,与project是完全一致的
- eg:
- 小技巧:CMake 的 ${} 表达式可以嵌套
因为 ${PROJECT_NAME} 求值的结果是 hellocmake
所以 ${${PROJECT_NAME}_VERSION} 相当于 ${hellocmake_VERSION}
进一步求值的结果也就是刚刚指定的 0.2.3 了。
- cmake_minimum_required 指定最低所需的 CMake 版本
假如你写的 CMakeLists.txt 包含了 3.15 版本才有的特性,如果用户在老版本上使用,就会出现各种奇怪的错误。
因此最好在第一行加个 cmake_minimum_required(VERSION 3.15)表示本 CMakeLists.txt 至少需要 CMake 版本 3.15 以上才能运行。
如果用户的 CMake 版本小于 3.15,会出现“CMake 版本不足”的提示。 - eg:my_course/course/11/02_project/10/CMakeLists.txt
- 建议使用arch linux而不是ubuntu,可以使用pacman更新cmake,使用screenfetch查看Linux版本号
- 可以通过 cmake --version 查看当前版本
- 也可以通过 CMAKE_VERSION 这个变量来获得当前 CMake 版本号。
- CMAKE_MINIMUM_REQUIRED_VERSION 获取 cmake_minimum_required中指定的“最小所需版本号”。
注意:cmake_minimum_required 不仅是“最小所需版本”
- 虽然名字叫 minimum_required,实际上不光是 >= 3.15 就不出错这么简单。根据你指定的不同的版本号,还会决定接下来一系列 CMake 指令的行为。(对 cmake_policy 有所影响)
- eg:my_course/course/11/02_project/11/CMakeLists.txt
(9)CMake常见变量——Project和CMake相关信息
- 链接:CMake常见变量——Project和CMake相关信息
CMAKE_BUILD_TOOL: 执行构建过程的工具。该变量设置为CMake构建时输出所需的程序。对于VS 6, CMAKE_BUILD_TOOL设置为msdev, 对于Unix,它被设置为make 或 gmake。 对于 VS 7, 它被设置为devenv. 对于Nmake构建文件,它的值为nmake。
CMAKE_DL_LIBS: 包含dlopen和dlclose的库的名称。
CMAKE_COMMAND: 指向cmake可执行程序的全路径。
CMAKE_CTEST_COMMAND: 指向ctest可执行程序的全路径。
CMAKE_EDIT_COMMAND: cmake-gui或ccmake的全路径。
CMAKE_EXECUTABLE_SUFFIX: 该平台上可执行程序的后缀。
CMAKE_SIZEOF_VOID_P: void指针的大小。
CMAKE_SKIP_RPATH: 如果为真,将不添加运行时路径信息。默认情况下是如果平台支持运行时信息,将会添加运行时信息到可执行程序当中。这样从构建树中运行程序将很容易。为了在安装过程中忽略掉RPATH,使用CMAKE_SKIP_INSTALL_RPATH。
CMAKE_GENERATOR: 构建工程的产生器。它将产生构建文件 (e.g. "Unix Makefiles", "Visual Studio 2019", etc.)
(10)一个标准的 CMakeLists.txt 模板
my_course/course/11/01_source/00/CMakeLists.txt
5.链接库文件
main.cpp 调用 mylib.cpp 里的 say_hello 函数
- eg:my_course/course/11/03_library/01/main.cpp
(1)改进方法1:mylib 作为一个静态库
- eg:my_course/course/11/03_library/02/CMakeLists.txt
(2)改进方法2:mylib 作为一个动态库
- eg:my_course/course/11/03_library/03/CMakeLists.txt
(3)改进方法3:mylib 作为一个对象库
- 对象库类似于静态库,但不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件
好处:给每个对象库中的.cpp文件单独增加flag编译选项,轻量级 - eg:my_course/course/11/03_library/04/CMakeLists.txt
- 说明:对象库类似于静态库,但不生成 .a 文件,只由 CMake 记住该库生成了哪些对象文件。
对象库是 CMake 自创的,绕开了编译器和操作系统的各种繁琐规则,保证了跨平台统一性。
在自己的项目中,我推荐全部用对象库(OBJECT)替代静态库(STATIC)避免跨平台的麻烦。
对象库仅仅作为组织代码的方式,而实际生成的可执行文件只有一个,减轻了部署的困难。
(4)静态库,对象库,动态库在自动剔除没有引用符号对象上的区别
静态库的麻烦:GCC 编译器自作聪明,会自动剔除没有引用符号的那些对象
- eg:my_course/course/11/03_library/05/CMakeLists.txt
对象库可以绕开编译器的不统一:保证不会自动剔除没引用到的对象文件
虽然动态库也可以避免剔除没引用的对象文件,但引入了运行时链接的麻烦
(5)add_library 无参数时,是静态库还是动态库?
会根据 BUILD_SHARED_LIBS 这个变量的值决定是动态库还是静态库。
- ON 则相当于 SHARED,OFF 则相当于 STATIC。
- 如果未指定 BUILD_SHARED_LIBS 变量,则默认为 STATIC。
因此,如果发现一个项目里的 add_library 都是无参数的,意味着你可以用:
cmake -B build -DBUILD_SHARED_LIBS:BOOL=ON来让他全部生成为动态库。 - 这也是命令行传递变量的一个eg。
- eg:
小技巧:设定一个变量的默认值
- 要让 BUILD_SHARED_LIBS 默认为 ON,可以用下图这个方法:
如果该变量没有定义,则设为 ON,否则保持用户指定的值不变。
这样当用户没有指定 BUILD_SHARED_LIBS 这个变量时,会默认变成 ON。
也就是说除非用户指定了 -DBUILD_SHARED_LIBS:BOOL=OFF 才会生成静态库,否则默认是生成动态库。 - eg:my_course/course/11/03_library/07
(6)常见坑点:动态库无法链接静态库
解决:让静态库编译时也生成位置无关的代码(PIC),这样才能装在动态库里
- eg:my_course/course/11/03_library/07/CMakeLists.txt
也可以只针对一个库,只对他启用位置无关的代码(PIC)
6.对象的属性
(1)set_property
my_course/course/11/04_property/01/CMakeLists.txt
(2)set_target_properties 批量设置多个属性
my_course/course/11/04_property/02/CMakeLists.txt
(3)通过全局的变量,让之后创建的所有对象都享有同样的属性
相当于改变了各个属性的初始默认值。
- 要注意此时 set(CMAKE_xxx) 必须在 add_executable 之前才有效。
my_course/course/11/04_property/03/CMakeLists.txt
(4)不要通过target_compile_options设置C++标准
对于 CXX_STANDARD 这种 CMake 本就提供了变量来设置的东西,就不要自己去设置 -std=c++17 选项,会和 CMake 自己设置好的冲突,导致出错。
- 请始终用 CXX_STANDARD 或是全局变量 CMAKE_CXX_STANDARD 来设置 -std=c++17 这个 flag,CMake 会在配置阶段检测编译器是否支持 C++17。
- CUDA 的 -arch=sm_75 也是同理,请使用 CUDA_ARCHITECTURES 属性。
- 再说了 -std=c++17 只是 GCC 编译器的选项,无法跨平台用于 MSVC 编译器。
- eg:my_course/course/11/04_property/04/CMakeLists.txt
(5)windows使用动态链接库
实现的地方加上dllexport,声明的地方加上dllimport
- eg:my_course/course/11/04_property/05/mylib/mylib.cpp
add_library中不需要把mylib.h加进去,所以link到可执行文件时,需要注意头文件的link,此外还需要注意可执行文件不需要link头文件,只用link库的传播功能:[学C++从Cmake学起]:中9.CMake 中的子模块
Windows常见问题:我链接了自己的 dll,但是为什么运行时会找不到?(Linux的target_link_libraries的target自动拥有RPATH,则不会出现问题)
- 这是因为你的 dll 和 exe 不在同一目录。
Windows 比较蠢,他只会找当前 exe 所在目录,然后查找 PATH,找不到就报错。而你的 dll 在其他目录,因此 Windows 会找不到 dll。
解决1:把 dll 所在位置加到你的 PATH 环境变量里去,一劳永逸。
解决2:把这个 dll,以及这个 dll 所依赖的其他 dll,全部拷贝到和 exe 文件同一目录下。
- 手动拷贝 dll 好麻烦,能不能让 CMake 把 dll 自动生成在 exe 同一目录?
归根到底还是因为 CMake 把定义在顶层模块里的 main 放在 build/main.exe。
而 mylib 因为是定义在 mylib 这个子模块里的,因此被放到了 build/mylib/mylib.dll。
Build目录下只有main.exe,却没有mylib.dll(main.exe和mylib.dll不在同一个目录)。所以找不到该dll - eg:my_course/course/11/04_property/05/CMakeLists.txt
通常在Linux情况下,若使用了target_link_libraries,那么target自然会有link的lib的RPATH的,参考:target_link_libraries
解决方法1:设置 mylib 对象的 xx_OUTPUT_DIRECTORY 系列属性
- 所以,可以设置 mylib 的这些属性,让 mylib.dll 文件输出到 PROJECT_BINARY_DIR,也就是项目根目录(main 所在的位置)。
这样 main.exe 在运行时就能找到 mylib.dll 了。
是的,为了伺候这睿智的 Windows 系统,需要设置全部 6 个属性(后者带DEBUG和RELEASE的),是不是非常繁琐? - eg:my_course/course/11/04_property/06/mylib/CMakeLists.txt
而 Linux 系统支持 RPATH(即:RUNTIME PATH,可以通过chrpath -l查看),CMake 会让生成出来可执行文件的 RPATH 字段指向他链接了的 .so 文件所在目录,运行时会优先从 RPATH 里找链接库,所以即使不在同目录也能找到。
7.链接第三方库
以需要使用 tbb 这个库为例
- eg:course/11/05_package/01/main.cpp
target_link_libraries链接器,会找到libtbb.so,从而找到libtbb.so.12,从而找到libtbb.so.12.5
直接链接 tbb 的缺点
- 如果这样直接指定 tbb,CMake 会让链接器在系统的库目录里查找 tbb,他会找到 /usr/lib/libtbb.so 这个系统自带的,但这对于没有一个固定库安装位置的 Windows 系统并不适用。
- 此外,他还要求 tbb 的头文件就在 /usr/include 这个系统默认的头文件目录,这样才能 #include <tbb/parallel_for.h> 不出错,如果 tbb 的头文件在其他地方就需要再加一个 target_include_directories 设置额外的头文件查找目录。
也可以直接写出全部路径,就是太硬核
- 这样也可以让没有默认系统路径的 Windows 找到安装在奇怪位置的 tbb……不过这样根本不跨平台,你这样改了别人如果装在不同地方就出错了。
- 此外,CMake 的路径分割符始终是 /。即使在 Windows 上,也要把所有的 \ 改成 /,这是出于跨平台的考虑。请放心,CMake 会自动在调用 MSVC 的时候转换成 \,你可以放心的用 ${x}/bin 来实现和 Python 的 os.path.join(x, ‘bin’) 一样的效果。
(1)find_package
更好的做法是用 CMake 的 find_package 命令。
- find_package(TBB REQUIRED) 会查找 /usr/lib/cmake/TBB/TBBConfig.cmake 这个配置文件,并根据里面的配置信息创建 TBB::tbb 这个伪对象(他实际指向真正的 tbb 库文件路径 /usr/lib/libtbb.so),之后通过 target_link_libraries 链接 TBB::tbb 就可以正常工作了。
- eg:course/11/05_package/02/CMakeLists.txt
TBB::tbb,TBB是包名,tbb是组件名
自带TBBConfig.cmake ,下面是arch linux的,使用pacman包管理器,所以安装在了系统目录,如果是ubuntu系统,则参考:15.安装第三方库 - 包管理器
为了使用方便,改造方法:ubuntu上已经装好了vcpkg,在cmake配置阶段只需要指定工具链位置即可
cmake -B build -DCMAKE_TOOLCHAIN_FILE="/home/jiwangreal/vcpkg/vcpkg/scripts/buildsystems/vcpkg.cmake" -DCMAKE_CXX_STANDARD=1
cmake --build build
若出现其他问题,按照报错修改CmakeLists.txt即可
- vcpkg手册官方网站,vcpk国内镜像下载源
TBB::tbb 的秘密:自带了一些 PUBLIC 属性
- TBB::tbb 是一个伪对象(imported),除了他会指向 /usr/lib/libtbb.so 之外,TBBConfig.cmake 还会给 TBB::tbb 添加一些 PUBLIC 属性,用于让链接了他的对象带上一些 flag 之类的。
比如,TBB 安装在 /opt/tbb 目录下,头文件在 /opt/tbb/include 里,那么这时 TBBConfig.cmake 里就会有:
target_include_directories(TBB::tbb PUBLIC /opt/tbb/include)
这样 main 在链接了 TBB::tbb 的时候也会被“传染”上 /opt/tbb/include 这个目录,不用调用者手动添加了比如 spdlog 的 spdlog-config.cmake 就会定义 SPDLOG_NOT_HEADER_ONLY 这个宏为 PUBLIC。从而实现直接 #include <spdlog/spdlog.h> 时候是纯头文件,而 find_package(spdlog REQUIRED) 时却变成预编译链接库的版本。(嗯,其实不是 PUBLIC 而是 INTERFACE,因为伪对象没有实体)
和古代 CMake 做对比:为什么 PUBLIC 属性的传播机制如此便利
(2)find_package(TBB REQUIRED) 和find_package(TBB CONFIG REQUIRED) 区别?
其实更好的是通过 find_package(TBB CONFIG REQUIRED),添加了一个 CONFIG 选项。这样他会优先查找 TBBConfig.cmake(系统自带的)而不是 FindTBB.cmake(项目作者常把他塞在 cmake/ 目录里并添加到 CMAKE_MODULE_PATH)。
这样能保证寻找包的这个 .cmake 脚本是和系统自带的 tbb 版本是适配的,而不是项目作者当年下载的那个版本的 .cmake 脚本。
- eg:course/11/05_package/03/CMakeLists.txt
- 当然,如果你坚持要用 find_package(TBB REQUIRED) 也是可以的。
没有 CONFIG 选项:先找 FindTBB.cmake,再找 TBBConfig.cmake,找不到则报错
有 CONFIG 选项:只会找 TBBConfig.cmake,找不到则报错 - 此外,一些老年项目(例如 OpenVDB)只提供 Find 而没有 Config 文件,这时候就必须
用 find_package(OpenVDB REQUIRED) 而不能带 CONFIG 选项。
/usr/lib/cmake/TBB/TBBConfig.cmake 长啥样?
- 不论是 TBBConfig.cmake 还是 FindTBB.cmake,这个文件通常由库的作者提供,在 Linux 的包管理器安装 tbb 后也会自动安装
这个文件。 - 少部分对 CMake 不友好的第三方库,需要自己写FindXXX.cmake 才能使用。
老年项目案例:OpenVDB(反面教材)
- 一些老年项目作者喜欢在项目里自己塞几个 FindXXX.cmake,然而版本可能和系统里的不一样,比如用 3.0 的 finder 去找 2.0 的包,容易出现一些奇奇怪怪的错误。
- 不建议大家这样用自己创建一个 cmake/ 目录来存用到的所有库的 finder,尽量用系统自带的,可以保证用的是系统自带库的那个配置。
(3)find_package(Qt5 REQUIRED) 出错了
eg:course/11/05_package/04/CMakeLists.txt
- 原因:Qt5 具有多个组件,必须指定你需要哪些组件
find_package 生成的伪对象(imported target)都按照“包名::组件名”的格式命名。
你可以在 find_package 中通过 COMPONENTS 选项,后面跟随一个列表表示需要用的组件。 - eg:course/11/05_package/05/CMakeLists.txt
常见问题:
- Windows 上找不到 Qt5 包怎么办?我明明安装了!
你是 Windows 系统,可能你安装了 Qt5,但是因为 Windows 系统的安装路径非常混乱,
没有固定的 /usr/lib 之类的默认路径可以搜索,所以出错了。
解决办法:
- 假设你的 Qt5 安装在 C:\Qt\Qt5.14.2,则你去找找这个目录:
C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake
你会看到他里面有个 Qt5Config.cmake 对吧。
现在,有四种方法让 CMake 找得到他。 - (1)第一种,设置 CMAKE_MODULE_PATH 变量,添加一下包含 Qt5Config.cmake 这个文件的目录路径 C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake,还要把 Windows 的 \ 全部换成Linux的 /,因为 CMake 是“亲 Unix”的构建系统。
eg:course/11/05_package/06/CMakeLists.txt - (2)第二种是设置 Qt5_DIR 这个变量为 C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake。这样只有 Qt5 这个包会去这个目录里搜索 Qt5Config.cmake,更有针对性。
course/11/05_package/06/CMakeLists.txt - (3)第三种(推荐),直接在命令行通过 -DQt5_DIR=”xxx” 指定,这样不用修改 CMakeLists.txt
- (4)第四种,还可以通过设置环境变量 Qt5_DIR 也是可以的,就是对 Windows 用户比较困难。
不指定 REQUIRED,找不到时不报错,只会设置 TBB_FOUND 为 FALSE
- 如果没有 REQUIRED 选项,找不到时将不会报错。
这样可以用于添加一些可选的依赖,如果没有也不要紧的那种,这时我们可以抛出一个警告。
找到了会把 TBB_FOUND 设为 TRUE,TBB_DIR 设为 TBBConfig.cmake 所在路径。
找不到会把 TBB_FOUND 设为 FALSE,TBB_DIR 为空。
这里我们在找到 TBB 时定义 WITH_TBB 宏,稍后 .cpp 里就可以根据这个判断。
如果找不到 TBB 可以 fallback 到保守的实现。
- eg:course/11/05_package/08/CMakeLists.txt
也可以用 TARGET 判断是否存在 TBB::tbb 这个伪对象,实现同样效果
- 找到,这里相当于true
- 也可以复合 if 的各种判断语句,例如NOT TARGET TBB::tbb AND TARGET Eigen3::eigen
表示找得到 TBB 但是找不到 Eigen3 的情况。
8.输出与变量
(1)message
在运行 cmake -B build 时,打印字符串(用于调试)
- eg:course/11/06_message/01/CMakeLists.txt
message(STATUS “…”) 表示信息类型是状态信息,有 – 前缀
- course/11/06_message/02/CMakeLists.txt
message(WARNING “…”) 表示是警告信息
message(AUTHOR_WARNING “…”) 表示是仅仅给项目作者看的警告信息,可以使用-Wno-dev关掉
message(FATAL_ERROR “…”) 表示是错误信息,会终止 CMake 的运行
- eg:course/11/06_message/03/CMakeLists.txt
message(SEND_ERROR “…”) 表示是错误信息,但之后的语句仍继续执行
- eg:
(2)message 可以用于打印变量
- course/11/06_message/04/CMakeLists.txt
如果 set 没加引号会怎样?会变成分号分割的列表
- eg:course/11/06_message/05/CMakeLists.txt
set(myvar hello world)
其实等价于:
set(myvar "hello;world")
Cmake里面的列表是用;进行分割的
如果 message 没加引号会怎样?会把列表里的字符串当成他的关键字
- 结论:除非确实需要列表,建议始终在你不确定的地方加上引号,例如:
set(sources "main.cpp" "mylib.cpp" "C:/Program Files/a.cpp")
message(“${sources}”)
8.变量与缓存
(1)重复执行 cmake -B build 会有什么区别?
可以看到第二次的输出少了很多,这是因为 CMake 第一遍需要检测编译器和 C++ 特性等比较耗时,检测完会把结果存储到缓存中
- 这样第二遍运行cmake -B build 时就可以直接用缓存的值,就不需要再检测一遍了。
(2)如何清除缓存
删 build 大法
- 然而有时候外部的情况有所更新,这时候 CMake 里缓存的却是旧的值,会导致一系列问题。
这时我们需要清除缓存,最简单的办法就是删除 build 文件夹,然后重新运行 cmake -B build。缓存是很多 CMake 出错的根源,因此如果出现诡异的错误,可以试试看删 build 全部重新构建。 - 经典 CMake 笑话:“99%的cmake错误可以用删build解决”“删 build 大法好”。
清除缓存,其实只需删除 build/CMakeCache.txt 就可以了
- 删 build 虽然彻底,也会导致编译的中间结果(.o文件)都没了,重新编译要花费很长时间。
如果只想清除缓存,不想从头重新编译,可以只删除 build/CMakeCache.txt 这个文件。 - 这文件里面装的就是缓存的变量,删了他就可以让 CMake 强制重新检测一遍所有库和编译器。
- build/CMakeCache.txt 的内容
(3)find_package 就用到了缓存机制
变量缓存的意义在于能够把 find_package 找到的库文件位置等信息,储存起来。
- 这样下次执行 find_package 时,就会利用上次缓存的变量,直接返回。
- 避免重复执行 cmake -B 时速度变慢的问题。
(4)设置缓存变量
语法是:set(变量名 “变量值” CACHE 变量类型 “注释”)
- eg:course/11/07_cache/02/CMakeLists.txt
缓存的 myvar 会出现在 build/CMakeCache.txt 里
常见问题:我修改了 CMakeLists.txt 里 set 的值,却没有更新?
- 为了更新缓存变量,有的同学偷懒直接修改 CMakeLists.txt 里的值,这是没用的。
因为 set(… CACHE …) 在缓存变量已经存在时,不会更新缓存的值! - CMakeLists.txt 里 set 的被认为是“默认值”因此不会在第二次 set 的时候更新。
- eg:course/11/07_cache/02/CMakeLists.txt
(4)缓存变量的更新方法
方法1:标准解法:通过命令行 -D 参数
- 更新缓存变量的正确方法,是通过命令行参数:cmake -B build -Dmyvar=world
- eg:course/11/07_cache/02/CMakeLists.txt
命令行 -D 参数太硬核了,有没有图形化的缓存编辑器?
- 在 Linux 中,可以运行 ccmake -B build 来启动基于终端的可视化缓存编辑菜单。
- 在 Windows 则可以 cmake-gui -B build 来启动图形界面编辑各个缓存选项。
- 当然,直接用编辑器打开 build/CMakeCache.txt 修改后保存也是可以的。
- CMakeCache.txt 用文本存储数据,就是可供用户手动编辑,或是被第三方软件打开并解析的。
方法2:暴力解决:删 build 大法
- 用万能的“删 build 大法”当然是可以的。这样重新执行的时候缓存变量不存在,
- 从而 set 会重新设置缓存的值为 world。建议初学者每次修改 CMakeLists.txt 时,都删一下 build/CMakeCache.txt 方便调试。
- eg:course/11/07_cache/02/CMakeLists.txt
方法3:
- 可以通过指定 FORCE 来强制 set 更新缓存
- set 可以在后面加一个 FORCE 选项,表示不论缓存是否存在,都强制更新缓存。
不过这样会导致没办法用 -Dmyvar=othervalue 来更新缓存变量。
(5)缓存变量的类型
STRING 字符串,例如 “hello, world”
FILEPATH 文件路径,例如 “C:/vcpkg/scripts/buildsystems/vcpkg.cmake”
PATH 目录路径,例如 “C:/Qt/Qt5.14.2/msvc2019_64/lib/cmake/”
BOOL 布尔值,只有两个取值:ON 或 OFF。
- 注意:TRUE 和 ON 等价,FALSE 和 OFF 等价;YES 和 ON 等价,NO 和 OFF 等价。
- eg:案例:添加一个 BOOL 类型的缓存变量,用于控制要不要启用某特性
course/11/07_cache/03/CMakeLists.txt
CMake 对 BOOL 类型缓存的 set 指令提供了一个简写:option
- eg:course/11/07_cache/04/CMakeLists.txt
option(变量名 "描述" 变量值)
等价于:
set(变量名 CACHE BOOL 变量值 "描述")
经典问题:option设为OFF了为什么他还是ON呀?
因为在CMakeLists.txt里直接改option是错的,官方解法是通过-D参数来改
- 刚刚说了,option 等价于 set(… CACHE BOOL …)。因此在CMakeLists.txt里改同样不会立即更新缓存里的值。
官方推荐做法是通过 -D变量名:BOOL=ON/OFF 来改缓存变量。这是cmake官方认为正确的缓存更新方式,但是很多人不知道,
还是傻傻的去改option里的值,然后发现没有效果,开始怀疑
cmake是不是出bug了,其实是官方宣传力度不够。
或者不要option了,直接用set加个FORCE即可始终强制更新缓存
- 当然最方便的还是删build大法,或者删build/CMakeCache.txt大法。
- 删build大法总能把缓存变量强制初始化为CMakeLists.txt里的值。
绕开缓存的方法:使用普通变量,但仅当没有定义时设定为默认值
- 一般来说CMake自带的变量(如CMAKE_BUILD_TYPE)都会这样设置。
- 这样项目的使用者还是可以用-D来指定参数,不过会在ccmake里看不到,也不会进缓存。
- Option唯一的好处就是可以让ccmake看得到
9.跨平台与编译器
(1)在 CMake 中给 .cpp 定义一个宏
也相当于gcc –DMU_MACRO=233
- eg:my_course/course/11/08_flags/01/CMakeLists.txt
根据不同的操作系统,把宏定义成不同的值
- eg:my_course/course/11/08_flags/02/CMakeLists.txt
(2)CMake 提供的一些简写变量
虽然名字叫 WIN32,实际上对 32 位 Windows 和 64 位 Windows 都适用
- APPLE 对于所有苹果产品(MacOS 或 iOS)都为真
- UNIX 对于所有 Unix 类系统(FreeBSD, Linux, Android, MacOS, iOS)都为真
- WIN32是bool
- eg:my_course/course/11/08_flags/03/CMakeLists.txt
CMake 还提供了一些简写变量:MSVC, CMAKE_COMPILER_IS_GNUCC
- eg:my_course/course/11/08_flags/09/CMakeLists.txt
(3)生成器表达式,简化成一条指令
语法:$<$<类型:值>:为真时的表达式>
比如 $<$<PLATFORM_ID:Windows>:MY_NAME=”Bill Gates”>
在 Windows 平台上会变为 MY_NAME=”Bill Gates”
其他平台上则表现为空字符串
- eg:my_course/course/11/08_flags/04/CMakeLists.txt
生成器表达式:如需多个平台可以用逗号分割
- eg:my_course/course/11/08_flags/05/CMakeLists.txt
(4)判断当前用的是哪一款 C++ 编译器
- eg:my_course/course/11/08_flags/06/CMakeLists.txt
也可以用生成器表达式判断编译器
生成器表达式也可以做复杂的逻辑判断
- eg:my_course/course/11/08_flags/07/CMakeLists.txt
CMAKE_CXX_COMPILER_ID 直接作为字符串变量
- eg:my_course/course/11/08_flags/10/CMakeLists.txt
(5)从命令行参数指定编译器CMAKE_CXX_COMPILER
第一次指定编译器,第二次要指定新的话,就要删除build
也可以通过环境变量 CXX 指定
相当于定义CMAKE_CXX_COMPILER
了解:CMAKE_GENERATOR
- eg:第一个是-G选项的内容
(6)vimrc
- 下载以及配置链接
- 生成compile_commands.json文件
结合ycm插件即可实现代码补全
1那个选项是导出编译命令,相当于set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
构建目录放在/tmp下面
导出的编译命令在build目录下面
10.分支与判断
(1)BOOL 类型的值
通常来说 BOOL 类型的变量只有 ON/OFF 两种取值。
- 但是由于历史原因,TRUE/FALSE 和 YES/NO 也可以表示 BOOL 类型。
- 个人建议同学们始终使用 ON/OFF 避免混淆。
- 链接:Cmake if
(2)if 的特点
不需要加 ${},会自动尝试作为变量名求值
- 由于历史原因,if 的括号中有着特殊的语法,如果是一个字符串,比如 MYVAR,则他会先看是否有 ${MYVAR} 这个变量。
如果有这个变量则会被替换为变量的值来进行接下来的比较,否则保持原来字符串不变。 - eg:course/11/09_branch/01/CMakeLists.txt
如果加了 ${} 会怎么样呢?
- if (${MYVAR} MATCHES “Hello”) 会被翻译成 if (Hello MATCHES “Hello”),但是因为找不到名为 Hello 的变量,所以会被直接当成普通的字符串来处理。
也就是 if (“Hello” MATCHES “Hello”) 从而会执行真分支,结果正常。 - eg:course/11/09_branch/02/CMakeLists.txt
正好定义了 Hello 这个变量呢?
- 然而假如存在 Hello 这个变量,其值为 “world”。那么 if (${MYVAR} MATCHES “Hello”) 会被翻译成 if (Hello MATCHES “Hello”)
而因为 Hello 这个变量名存在,所以会被(出于历史原因的)if 进一步求值:if (“world” MATCHES “Hello”) 从而会执行假分支,结果不正常了。 - eg:course/11/09_branch/03/CMakeLists.txt
解决:用引号包裹起来,防止被当做变量名
- 初学者如果搞不明白,可以把所有不确定的地方都套上一层引号,例如”${MYVAR}”,这样就可以避免被 if 当做变量名来求值了。
- eg:course/11/09_branch/05/CMakeLists.txt
11.变量与作用域
(1)变量的传播规则:父会传给子
父模块里定义的变量,会传递给子模块。
- eg:course/11/10_scope/01/CMakeLists.txt
(2)变量的传播规则:子不传给父
但是子模块里定义的变量,不会传递给父模块。
- eg:course/11/10_scope/02/CMakeLists.txt
如果父模块里本来就定义了同名变量,则离开子模块后仍保持父模块原来设置的值。
- eg:course/11/10_scope/03/CMakeLists.txt
(3)子模块需要向父模块里传变量
可以用 set 的 PARENT_SCOPE 选项,把一个变量传递到上一层作用域(也就是父模块)。
- eg:course/11/10_scope/04/mylib/CMakeLists.txt
- 加上PARENT_SCOPE可以影响到外面去。set(MYVAR ON PARENT_SCOPE)与set(MYVAR ON)不一样,前者会传到父模块
- 链接:CMake-command
如果父模块里没有定义 MYVAR 的话,也可以用缓存变量向外部传变量(不推荐)。但是这样就不光父模块可见了,父模块的父模块,到处都可见。
- 缓存变量是全局的
- eg:course/11/10_scope/05/mylib/CMakeLists.txt
(4)除了父子模块之外还有哪些是带独立作用域的
include 的 XXX.cmake 没有独立作用域,甚至子模块include,外面的父模块也可以看得到
add_subdirectory 的 CMakeLists.txt 有独立作用域
macro 没有独立作用域,插入执行,里面的变量会暴露出来
function 有独立作用域,里面的变量不会暴露出来,要返回值的话得用PARENT_SCORE
(因此 PARENT_SCORE 也可以用于 function 的返回值)
- 参考:Cmake入门之——Set方法(六)
(5)环境变量的访问方式:$ENV{xx}
用 ${xx} 访问的是局部变量,局部变量服从刚刚所说的父子模块传播规则。
而还有一种特殊的方式可以访问到系统的环境变量(environment variable):$ENV{xx}。
- eg:比如 $ENV{PATH} 就是获取 PATH 这个环境变量的值。
(6)缓存变量的访问方式:$CACHE{xx}
此外,还可以用 $CACHE{xx} 来访问缓存里的 xx 变量。
- 缓存变量和环境变量是不论父子模块都共用的,没有作用域一说。
- 这里没有set这个变量,直接从缓存里面读取的
${xx} 找不到局部变量时,会自动去找缓存变量
- ${xx} 当找不到名为 xx 的局部变量时,就会去在缓存里查找名为 xx 的缓存变量。
因此这里 CMAKE_BUILD_TYPE 虽然在代码里没被 set,但是他被-D参数固定在缓存里了。 - 所以 ${CMAKE_BUILD_TYPE} 自动变成 $CACHE{CMAKE_BUILD_TYPE} 从而找到变量。
- 所以这就是可以通过-D参数传递一个变量的原因。
- eg:course/11/10_scope/06/CMakeLists.txt
(7)if (DEFINED xx) 判断某变量是否存在
if (DEFINED MYVAR) 可以判断是否存在 MYVAR 这个局部变量或缓存变量。
- eg:course/11/10_scope/07/CMakeLists.txt
值得注意的是:空字符串不代表变量不存在。因此即使是空字符串 DEFINED 也认为存在。
if (xx) 就可以判断某变量是否存在且不为空字符串
- eg:course/11/10_scope/08/CMakeLists.txt
if (DEFINED ENV{xx}) 判断某环境变量是否存在
- 因为 $ENV{xx} 代表环境变量,因此在 set 和 if 中也可以用 ENV{xx} 来表示环境变量。
因为 set 的第一参数和 if 的参数都是不加 $ 的,所以要设置 ${x} 就变成了 set(x …)。而设置 $ENV{x} 自然就是 set(ENV{x} …) 咯。
同理还可以用 if (DEFINED CACHE{x}) 判断是否存在这个缓存变量,但是 set(CACHE{x} …) 就不行。因为已经有CACHE:BOOL那个东西了
从 bash 设置环境变量
12.其他小建议
(1)CCache:编译加速缓存
用法:把 gcc -c main.cpp -o main 换成 ccache gcc -c main.cpp -o main 即可
在 CMake 中可以这样来启用 ccache(就是给每个编译和链接命令前面加上 ccache):
- eg:course/11/template/CMakeLists.txt
- CCache 官方网站
添加一个 run 伪目标,用于启动主程序(可执行文件)
- 创建一个 run 伪目标,其执行 main 的可执行文件。
- 这里用了生成器表达式 $<TARGET_FILE:main>,会自动让 run 依赖于 main。
- 如果不放心有没有自动依赖上,手动加一个 add_dependencies(run main) 也是可以的。
- 这样就可以在命令行运行 cmake --build build --target run 来启动 main.exe 运行了。
而不必根据不同的平台,手动写出 build/main 或是 build\main.exe。
再加一个 configure 伪目标,用于可视化地修改缓存变量
- 这样就可以用 cmake --build build --target configure 来启动 ccmake 修改缓存了。
- 这在 Linux 上相当于 ccmake -B build,Windows 则是 cmake-gui -B build。