对从未创建过 CMake 项目的我来说,工作中老是会遇到一些开源项目使用它。我也就是按说明文档用 cmake-gui 点两下,生成 Visual Stuio 项目文件,然后编译。
尝试学习过,但多是通篇下来没有主线和重点,全是介绍 CMake 语法糖,让人摸不清头脑,搞不清到底要记哪些重点知识,如何组织这些碎片知识。
今天台风一刮,脑中突发奇想:CMake 本质不就是创建一个项目吗?像 Visual Studio 创建解决方案一样,下一步...下一步...完成。沿着这条主线再看 CMake 似乎一下就清晰了。
这里拿 Visual Studio 类比,CMake 本质就是创建一个解决方案 (.sln),然后向解决方案中不断添加项目 (.vcxproj),再然后就是为项目添加源码,以及配置项目属性了。以下章节我将沿着这条主线展开。
一、创建解决方案 (.sln)
像 Visual Studio 一样,第一步创建解决方案 (.sln),CMake 非常简单,创建一个文本文件即可,文件名必须叫:CMakeLists.txt
> mkdir HelloCMake
> cd HelloCMake
> fsutil file createNew CMakeLists.txt 0
然后我们就可以打开 CMakeLists.txt 文件进行编辑了。
1. CMake 版本
像使用不同版本的 Visual Studio 一样,CMake 也存在一些版本间的差异,所以上来先明确我们对 CMake 最低版本的要求。
cmake_minimum_required(VERSION 3.12)
它的语法也非常简单,详见官方文档 (cmake_minimum_required)。
cmake_minimum_required(VERSION <min>[...<policy_max>] [FATAL_ERROR])
2. 创建解决方案
project(HelloCMake)
这句非常简单,就是创建一个 HelloCMake.sln 文件。它的语法也非常简单,详见官方文档 (project)。
project(<PROJECT-NAME> [<language-name>...])
project(<PROJECT-NAME>
[VERSION <major>[.<minor>[.<patch>[.<tweak>]]]]
[DESCRIPTION <project-description-string>]
[HOMEPAGE_URL <url-string>]
[LANGUAGES <language-name>...])
到这里,我们已经完成了解决方案的创建,完整 CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.12)
project(HelloCMake)
有了这个文件,我们便可以生成 .sln 工程了,一般我们将工程生成到 build 文件夹下:
> mkdir build
> cd build
> cmake ..
当然你也可以指定 Visual Studio 版本等信息,可以通过 cmake --help 查看参数详情,这里不做重点介绍。
二、添加项目 (.vcxproj)
为了简单起见,我这里创建 3 个项目,且三个项目的源代码都与 CMakeLists.txt 文件在同级目录。文件列表如下:
CMakeLists.txt
----
app.h
app.cpp
app2.cpp
----
mylib.h
mylib.cpp
mylib2.cpp
----
mydll.h
mydll.cpp
mydll2.cpp
CMakeLists.txt 文件内容如下:
cmake_minimum_required(VERSION 3.12)
# 1. 创建解决方案
project(HelloCMake)
# 2. 向解决方案中添加应用程序项目 (exe)
add_executable(app app.cpp app2.cpp)
# 3. 向解决方案中添加静态库项目 (lib)
add_library(mylib STATIC mylib.cpp mylib2.cpp)
# 4. 向解决方案中添加动态库项目 (dll)
add_library(mydll SHARED mydll.cpp mydll2.cpp)
此时运行 cmake 命令,就会为我们生成一个 HelloCMake.sln,它包含有 3 个项目分别是 app.vcxproj、mylib.vcxproj、mydll.vcxproj。至于 add_executable 等函数的语法详情请自行移步至 官方文档 (index) 搜索。
1. 添加文件
刚才我们添加源代码的方式过于简单,对于复杂的源码目录结构,我们需要用于几个辅助函数:
# 将<dir>中扫描到的所有源代码列表赋值给变量<variable>,不包含子文件夹
aux_source_directory(<dir> <variable>)
# 将<value>列表赋值给变量<variable>
set(<variable> <value>... [PARENT_SCOPE])
# 可指定过滤条件
file(...)
直接上示例,我为我的 3 个项目分别再创建 3 个文件夹,每个文件夹下再添加一些文件:
+ src4app
-- app_header1.h
-- app_header2.h
-- app_src1.cpp
-- app_src2.cpp
+ src4lib
-- lib_header1.h
-- lib_header2.h
-- lib_src1.cpp
-- lib_src2.cpp
+ src4dll
-- dll_header1.h
-- dll_header2.h
-- dll_src1.cpp
-- dll_src2.cpp
CMakeLists.txt 文件完整内容如下:
cmake_minimum_required(VERSION 3.12)
project(HelloCMake)
aux_source_directory(src4app APP_SOURCE_LIST)
aux_source_directory(src4lib LIB_SOURCE_LIST)
aux_source_directory(src4dll DLL_SOURCE_LIST)
set(APP_SOURCE_LIST ${APP_SOURCE_LIST} app.cpp app2.cpp)
set(LIB_SOURCE_LIST ${LIB_SOURCE_LIST} mylib.cpp mylib2.cpp)
set(DLL_SOURCE_LIST ${DLL_SOURCE_LIST} mydll.cpp mydll2.cpp)
file(GLOB APP_HEADER_LIST app.h "src4app/*.h")
file(GLOB LIB_HEADER_LIST "mylib.h" src4lib/*.h)
file(GLOB DLL_HEADER_LIST mydll.h src4dll/*.h)
add_executable(app ${APP_SOURCE_LIST} ${APP_HEADER_LIST})
add_library(mylib STATIC ${LIB_SOURCE_LIST} ${LIB_HEADER_LIST})
add_library(mydll SHARED ${DLL_SOURCE_LIST} ${DLL_HEADER_LIST})
2. 筛选文件
拿到文件列表后,我们可能需要对该列表进行一些增删改查操作,这时需要用到 list 函数,详见官方文档 (list)
Reading
list(LENGTH <list> <out-var>)
list(GET <list> <element index> [<index> ...] <out-var>)
list(JOIN <list> <glue> <out-var>)
list(SUBLIST <list> <begin> <length> <out-var>)
Search
list(FIND <list> <value> <out-var>)
Modification
list(APPEND <list> [<element>...])
list(FILTER <list> {INCLUDE | EXCLUDE} REGEX <regex>)
list(INSERT <list> <index> [<element>...])
list(POP_BACK <list> [<out-var>...])
list(POP_FRONT <list> [<out-var>...])
list(PREPEND <list> [<element>...])
list(REMOVE_ITEM <list> <value>...)
list(REMOVE_AT <list> <index>...)
list(REMOVE_DUPLICATES <list>)
list(TRANSFORM <list> <ACTION> [...])
Ordering
list(REVERSE <list>)
list(SORT <list> [...])
来举例说明:
# 文件列表:a.cpp;b.cpp;c.cpp
set(MyFileList a.cpp b.cpp c.cpp)
# 将文件列表的长度赋值给变量len
list(LENGTH MyFileList len)
# 打印len值:"len = 3"
message("len = ${len}")
其他操作就不详述了,总之记录它可以实现对列表的增删改查即可。
三、项目属性设置
创建完解决方案,添加好项目及其源码后,接下来就是设置项目的属性了。
1. 头文件包含路径
对应 Visual Studio 的项目属性 -> 附加包含目录,直接上示例:
# 设置所有项目
include_directories(
src4lib/include
src4dll/include)
# 设置指定项目
target_include_directories(mylib PRIVATE boost/include)
2. 宏定义
对应 Visual Studio 的项目属性 -> 预处理器定义,上示例:
# 设置所有项目
add_compile_definitions(NOMINMAX)
# 设置指定项目
target_compile_definitions(mydll PRIVATE LIBADD_API_EXPORTS)
3. 编译选项
对应 Visual Studio 的项目属性 -> C/C++ -> 所有选项 -> 附加选项,上示例:
target_compile_options(mylib PRIVATE /utf-8)
4. 链接器
对应 Visual Studio 的项目属性 -> 链接器,上示例:
# 相关函数
link_directories([AFTER|BEFORE] directory1 [directory2 ...])
target_link_libraries(<target> ... <item>... ...)
target_link_directories(<target> [BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
# 示例:app项目,链接了mylib和mydll
target_link_libraries(app mylib mydll)
到这里,项目常用的配置信息就差不多全涉及了。CMake 可以设置更多项目细节参数,用到的时候再去找资料即可,没必要记忆。
四、多层 CMakeLists.txt
每个项目都应该有自己独立的 CMakeLists.txt 文件,而不是全混在同一个文件中。经过上述章节的介绍,我们已经可以手写一个简单的小项目了,把各个小项目放在各自的文件夹下,然后在文件夹外部,写一个总的 CMakeLists.txt,把其他所有小项目文件夹汇总即可。
add_subdirectory(mylib)
add_subdirectory(mydll)
五、功能开关
项目有时需要根据不同的编译环境,配置不同的选项,这里就需要一些开关来控制,上示例:
# option用于定义布尔型选项,可以在构建项目时启用或禁用特定的功能。[value]通常是 ON 或 OFF
option(<variable> "<help_text>" [value])
# 示例
option(ENABLE_LOGGING "Enable logging system" ON)
if (ENABLE_LOGGING)
add_compile_definitions(ENABLE_LOGGING)
endif()
六、CMake 内置变量
CMake 内置了很多变量方便我们使用,这里列几个常用的变量:
${PROJECT_SOURCE_DIR}
# 工程根目录
${PROJECT_BINARY_DIR}
# cmake输出目录
${PROJECT_NAME}
# sln 名称
${CMAKE_CURRENT_SOURCE_DIR}
# 当前处理的 CMakeLists.txt 所在的路径
${CMAKE_CURRENT_BINARY_DIR}
# 目标编译目录
${CMAKE_CURRENT_LIST_DIR}
# CMakeLists.txt 的完整路径
${EXECUTABLE_OUTPUT_PATH}
# 重新定义目标二进制可执行文件的存放位置
${LIBRARY_OUTPUT_PATH}
# 重新定义目标链接库文件的存放位置
相信到这里,大家对 CMake 就有了非常清晰的认识。它的出现是为了解决跨平台项目的创建问题,如 Visual Studio 创建的项目 Linux 下无法使用,Makefile 在 Windows 系统下又不好编译等。CMake 会为我们的代码,自动生成 Visual Studio 工程文件,或 Makefile 文件,而无需我们去处理。
参考文档:
- [1] cmake.org/cmake/help/… "官方文档"
- [2] zhuanlan.zhihu.com/p/267803605 "Cmake 语法与实战入门"
- [3] zhuanlan.zhihu.com/p/663370110 "CMake 中的 aux_source_directory 命令深入解析"