- 案例:需要使用 tbb 这个库
- 直接链接 tbb 的缺点
- 也可以直接写出全部路径,就是太硬核
- 更通用的方式:
find_package TBB::tbb的秘密:自带了一些PUBLIC属性- 和古代 CMake 做对比:为什么
PUBLIC属性的传播机制如此便利 - 和
find_package(TBB CONFIG REQUIRED)有什么区别? /usr/lib/cmake/TBB/TBBConfig.cmake长啥样?- 老年项目案例:
OpenVDB(反面教材) find_package(Qt5 REQUIRED)出错了- 常见问题:小彭老师,Windows 上找不到 Qt5 包怎么办?我明明安装了!
- 不指定
REQUIRED,找不到时不报错,只会设置TBB_FOUND为FALSE - 这样在
.cpp里可以判断WITH_TBB宏,找不到 TBB 时退化到串行for循环 - 也可以用
TARGET判断是否存在TBB::tbb这个伪对象,实现同样效果
案例:需要使用 tbb 这个库

直接链接 tbb 的缺点
如果这样直接指定 tbb,CMake 会让链接器在系统的库目录里查找 tbb,他会找到/usr/lib/libtbb.so 这个系统自带的,但这对于没有一个固定库安装位置的 Windows 系统并不适用。
此外,他还要求 tbb 的头文件就在 /usr/include 这个系统默认的头文件目录,这样才能#include 不出错,如果 tbb 的头文件在其他地方就需要再加一个target_include_directories设置额外的头文件查找目录。
也可以直接写出全部路径,就是太硬核
也可以直接写出全部路径,这样也可以让没有默认系统路径的 Windows 找到安装在奇怪位置的 tbb……不过这样根本不跨平台,你这样改了别人如果装在不同地方就出错了。
顺便一提,CMake 的路径分割符始终是 /。即使在 Windows 上,也要把所有的 \ 改成 /,这是出于跨平台的考虑。请放心,CMake 会自动在调用 MSVC 的时候转换成 \,你可以放心的用 ${x}/bin 来实现和 Python 的 os.path.join(x, ‘bin’) 一样的效果。

毕竟大多数操作系统都是 Unix-like 嘛……就 Windows 喜欢搞特殊。 cd /d C:\\Program\ Files\ \(x86\)\\Microsoft\ Visual\ Studio\\2019\\怎么路径里动不动夹杂几个转移符、空格、特殊符号?这谁顶得住啊。 高情商:Windows 是最适合练习你 C 语言转移符使用水平的地方。
更通用的方式: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 就可以正常工作了。
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 这个目录,不用调用者手动添加了。
再比如,TBB::tbb 链接了另一个库 Blosc::blosc,那这个库也会自动链接到 main 上,无需调用者手动添加。 比如 spdlog 的 spdlog-config.cmake 就会定义 SPDLOG_NOT_HEADER_ONLY 这个宏为 PUBLIC。从而实现直接 #include 时候是纯头文件,而 find_package(spdlog REQUIRED) 时却变成预编译链接库的版本。(嗯,其实不是 PUBLIC 而是 INTERFACE,因为伪对象没有实体)
和古代 CMake 做对比:为什么 PUBLIC 属性的传播机制如此便利
古代 CMake:
现代 CMake:
和 find_package(TBB CONFIG REQUIRED) 有什么区别?

其实更好的是通过 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 选项。
/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,尽量用系统自带的,可以保证用的是系统自带库的那个配置。
find_package(Qt5 REQUIRED) 出错了
原因:Qt5 具有多个组件,必须指定你需要哪些组件
find_package 生成的伪对象(imported target)都按照“包名::组件名”的格式命名。 你可以在 find_package 中通过 COMPONENTS 选项,后面跟随一个列表表示需要用的组件。

测试一下能否找到 Qt 的头文件并编译通过


常见问题:小彭老师,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 找得到他。
- 第一种是设置
CMAKE_MODULE_PATH变量,添加一下包含Qt5Config.cmake这个文件的目录路径C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake,当然刚刚说了尽管你是 Windows 还是要把\全部换成/,因为 CMake 是“亲 Unix”的构建系统。 是的,学个编程跟隔壁史地政一样,有地缘因素在里边……

- 更好的方法:设置
<包名>_DIR变量指向<包名>Config.cmake所在位置. 第二种是设置Qt5_DIR这个变量为C:\Qt\Qt5.14.2\msvc2019_64\lib\cmake。 这样只有 Qt5 这个包会去这个目录里搜索Qt5Config.cmake,更有针对性。

- 第三种(推荐),直接在命令行通过
-DQt5_DIR="xxx"指定,这样不用修改 CMakeLists.txt

- 第四种,还可以通过设置环境变量
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 到保守的实现。
这样在 .cpp 里可以判断 WITH_TBB 宏,找不到 TBB 时退化到串行 for 循环
也可以用 TARGET 判断是否存在 TBB::tbb 这个伪对象,实现同样效果

也可以复合 if 的各种判断语句,例如 NOT TARGET TBB::tbb AND TARGET Eigen3::eigen 表示找得到 TBB 但是找不到 Eigen3的情况。

