文章目录
前言
记录这段时间对CMake的学习浅显认识,实际生产没使用过,当作抛砖引玉,希望各位大触指出我的错误。
关联项目:https://github.com/Lzeyuan/LearnOpenGLWithCMake
1 快速开始
cmake_minimum_required(VERSION 3.25)
project(00_QuickStart VERSION 1.0 LANGUAGES C CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
add_executable(
${PROJECT_NAME}
main.cpp
)
2 子目录(模块)
复杂度不会消失,但是可以转移,分而治之
2.1 include和add_subdirectory
include等价于#include
stackoverflow的QA:cmake: add_subdirectory() vs include()
主要区别:
- 使用add_subdirectory会把目录结构体现在生成目录里
- 使用add_subdirectory会创建一个新作用域
子项目和正常项目差不多,不过要考虑共享信息给引用项目。
# glad子项目
project(glad)
# 默认STATIC,可不写
add_library(
${PROJECT_NAME}
STATIC
src/glad.c
)
# 这里把头文件 PUBLIC 共享给引用的项目
target_include_directories(
${PROJECT_NAME}
PUBLIC
include
)
如果是纯动态库项目,应该怎么封装呢?请看下一节。
3 动态库使用和Install指令
一般自己项目拆成静态库。
3.1 动态库
3.1.1 封装动态库文件
书接上回,包装只有动态库文件的项目应该怎么做,这里把上节的glfw项目替换成现在项目这样只有动态库和头文件。
那么现在要做的重点就是用INTERFACE
选项,见下面CMakeLists.txt
代码
该选项和PUBLIC
相似,不过内容不给自身项目使用
也就说target_include_directories
添加头文件,然后使用这个选项,自身项目写代码时还去include这头文件会提示找不到。
# glfw
# │ CMakeLists.txt
# │
# ├─bin
# │ glfw3.dll
# │
# ├─include
# │ └─GLFW
# │ glfw3.h
# │ glfw3native.h
# │
# └─lib
# glfw3dll.lib
# glfw动态库子项目
project(glfw)
# 仅共享动态库,没有源文件
add_library(
${PROJECT_NAME}
INTERFACE
)
# 这里把头文件共享给引用的项目
target_include_directories(
${PROJECT_NAME}
INTERFACE
include
)
target_link_directories(
${PROJECT_NAME}
INTERFACE
lib
)
target_link_libraries(
${PROJECT_NAME}
INTERFACE
glfw3dll
)
3.1.2 使用场景
开头所写 一般自己项目拆成静态库。
为什么呢?
因为动态库巨麻烦,有一堆需要考虑的事情,作者没有全面学习,这里写的是个人见解。
目的:
- 节省内存:需要时加载
- 节省外存:只有一份副本,不会链接进执行文件里
- 热更新
- 加快生成速度,编译可以并行,链接只能串行,把链接时机推后
- 统一库的版本,不同库可能链接统一库的不同版本导致符号冲突,或者执行时因为内存布局不同导致崩溃,比如MSVC例子
缺点:
- 符号冲突编译器不能帮忙检测,比如:两个动态库写了一样的函数名。
- 依赖关系:典型的protobuf库版本问题,要手动处理依赖库。
CMake默认链接MSVC的动态库,xmake则是静态库。
3.2 Install 指令
3.2.1 找不到glfw3.dll
修改完后运行一下项目,会报错找不到glfw3.dll
把
本地路径\LearnOpenGLWithCMake\02_DynamicAndInstall\3rd\glfw\lib\glfw3.dll
复制到
本地路径\LearnOpenGLWithCMake\02_DynamicAndInstall\out\build\x64-debug
复制到这个目录下,然后在运行程序,可以正常运行。
当然也可以把DLL所在目录加入到系统环境里,或者在程序使用系统api路径加载动态库。
那为什么选择第一种方法?因为我看其它软件都是这么做的。
Windosw加载顺序参考:https://learn.microsoft.com/zh-cn/windows/win32/dlls/dynamic-link-library-search-order
3.2.2 Install使用
上面步骤可以可以用Install指令简化:
project(glfw)
add_library(
${PROJECT_NAME}
INTERFACE
)
target_include_directories(
${PROJECT_NAME}
INTERFACE
include
)
target_link_directories(
${PROJECT_NAME}
INTERFACE
lib
)
target_link_libraries(
${PROJECT_NAME}
INTERFACE
glfw3dll
)
# 安装dll
install(
FILES ${CMAKE_CURRENT_SOURCE_DIR}/lib/glfw3.dll
DESTINATION bin
)
现在在VS的运行目标找到02_DynamicAndInstall(安装)
然后运行,找不到DLL的提示就消失了,取而代之的是正常运行。
其它IDE在运行安装后可以到out/install
下查看,程序和dll都在一个目录下,直接运行即可。
3.3 简化 git clone
那么介绍了回字的两种写法(导入第三方库的两种方法),现在在介绍一种。
这等同于 git clone https://github.com/glfw/glfw.git
然后 git checkout 自取分支名 3.3.9
。
...
include(fetchcontent) # 照写,不需要修改
fetchcontent_declare(
glfw #库名字
GIT_REPOSITORY https://github.com/glfw/glfw.git # 仓库地址
GIT_TAG 3.3.9 # 库版本
SOURCE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/3rd/glfw # 指定库下载地址
)
fetchcontent_makeavailable(glfw)
...
不过我还是推荐第一节课的方法使用 git submodule
管理版本,然后视情况编译成动态库还是静态库。
当然包管理的方法我也是推荐的,下节会介绍。
4 CMakePresets和Vcpkg
仍然是分而治之
4.1 CMakePresets
CMakePresets这个东西前几个项目就有了,现在终于写到他了。
4.1.1 作用
还是那句话,转移复杂度,分而治之。
便于用户指定通用的配置、生成和测试选项,并与他人共享。
目前只用到了配置和生成。
4.1.2 CMakePresets和CMakeUserPresets
CMakePreset.json 加入版本控制,配置宏之类的参数
CMakeUserPreset.json 不加入版本控制,配置本地开发环境参数
写死的配置写在CMakeLists.txt
里,根据情况配置的写在CMakePresets.json
里,和开发者环境有关的写在CMakeUserPreset.json
里。
为了演示,这一节项目上传了CMakeUserPresets.json
,记得修改VCPKG_TOOLCHAIN_FILE
。
比如:
CMAKE_POLICY_DEFAULT_CMP0091
这个规则和Windows平台相关的就写在CMakePresets.json
里- 生成目录、安装目录、平台架构等等的配置也写在
CMakePresets.json
里 VCPKG_TOOLCHAIN_FILE
是配置vcpkg.cmake
的文件路径,应写在CMakeUserPreset.json
里
具体查看文件,都是KV值很好懂。
4.2 Vcpkg
不依赖cmake版本,因为他是作为工具链传入cmake。
4.2.1 vcpkg.json管理独立环境
和python的conda一样,每个项目都可以创建专属的环境安装特定版本的包,不和其它环境冲突。
必须要使用overrides
才能指定版本号。
不要指定版本号,详见吐槽。
必须添加builtin-baseline,若要添加初始 “builtin-baseline”,请使用 vcpkg x-update-baseline --add-initial-baseline。 若要更新清单中的基线,请使用 vcpkg x-update-baseline。
{
"name": "cmake-learn",
"version-semver": "0.0.1",
"dependencies": [
{
"name": "glfw3",
"platform": "(windows & x64)"
},
{
"name": "assimp",
"version>=": "5.3.1",
"platform": "(windows & x64)"
}
],
"overrides": [
{
"name": "glfw3",
"version": "3.3.9"
}
],
"builtin-baseline": "7032c5759f823fc8699a5c37d5f7072464ccb9a8"
}
4.2.2 指定平台
很坑,这个选项意思是:满足这个条件才安装
,具体安装配置参考:三元组参考。
比如:"platform": "(windows & x64 & static)"
,但是没有配置
VCPKG_TARGET_TRIPLET为x64-windows-static
他不会安装到项目环境里,因为默认是x64-windows
。
甚至不写都行,直接看VCPKG_TARGET_TRIPLET
设置。
4.3 吐槽
和构建构建工具一样C++没几个工具是用的舒服的。
4.3.1 vcpkg
每个库都会有版本号更新,而对于vcpkg,某个版本号下修bug的提交(不改版本号),会视为一次port-version
记录起来。
这就导致一个坑,在使用指定版本的时候,比如某个库在3.3.9版本下更新了10次,就是说会有0~9的port-version
,你指定3.3.9版本,他不会去安装最新的修复bug那一次提交(3.3.9#10),而是安装第一次发布时的提交,即3.3.9#0,而这种情况一般就是想要最新的更新。
本来是有选项显示某个库某个版本的port-version
,但是删除了Remove command x-history
那怎么解决呢?查看 vcpkg安装目录下/version/安装库名开头字母-/安装库名.json
,里面就有对应port-version
记录了。
4.3.2 导入第三方
目前介绍了四种协回字的方法:
- 复制项目到子项目
- CMake自带git克隆,简化第一个方法的流程
- 配置编译好的动态库项目
- 包管理,比如vcpkg
这里我还推荐第三种方法,理由如下:
- 方便处理版本依赖问题,这个时候可以手动配置依赖关系,经典的protobuf版本问题,还有标准库实现。
- 减少编译速度,比如后面会用到的
assimp
库编译很久,每次删除编译缓存都要重新构建,而且库还很大,每次链接都很久。 - 减少了一层包管理器坑,这个和C++特性有关,java用的maven就没有这种担心。