CMake 和 CMakeLists 配置分享
说明:
本文假定各位对命令提示符, shell 等有基本了解. 能够独立编写一些命令, 诸如 touch main.cpp , new-item main.cpp 等等.
本文的编程部分在MSVC环境中完成, 部分是在 mingw64, 使用 msys2 配置的环境中完成的. windows 用户, 请安装 VS 完成前者工作, 跟随这个教程完成后者工作: msys2 环境配置 - KBLEric的文章 - 知乎 https://zhuanlan.zhihu.com/p/682789581.
对于 Linux 用户, 没有探究可能存在的 Linux 社区版本 MSVC 编译系统的必要, 使用 GCC 的 gcc 和 g++ 就可以了. 或者也可以使用 clang.
本文面向的对象是认识 C++ 基本编程操作, 至少对于多文件编程有一定程度的了解. 类似下面结构的代码, 能够独立完成.
// file add.h
# ifndef ADD_H
# define ADD_H
double add(double a, double b);
# endif
// file add.cpp
# include "add.h"
double add(double a, double b){
return a+b;
}
// file main.cpp
# include <iostream>
int main (void){
std::cout << add(3.0, 2.0);
return 0;
}
假定各位能够独立编写以上结构代码, 然后能够独立给出编译此文件的命令:
# linux
g++ add.cpp main.cpp && ./a.out
# or
g++ add.h add.cpp main.cpp && ./a.out
# windows mingw
g++ add.cpp main.cpp && ./a.exe
# or
g++ add.h add.cpp main.cpp && ./a.exe
概述
CMake 是一款用于配置项目的软件, 至少到上述所学内容为止, 它能够处理 include 文件和 src 文件之间关系的问题. CMake 不能用来直接生成项目结果, 必须借助已有构建工具, 比如 MakeFile , Ninja 等等. 本文不会详细介绍 Makefile 和 ninja 的编写. 所以各位放心食用, 不用担心.
安装
windows
对于 windows 环境下的安装, 十分简单, 到这个地址: https://cmake.org/download/ 找到最新版本找到对应系统位数的msi版本下载即可. 如果下载 zip 版本, 请自行配置环境变量.
到我编写此文为止, cmake 最新版本是 3.31.4 . 进来先看到一个欢迎界面, 下一步以后, 来到协议界面, 点击接受协议以后 NEXT, 进入配置安装界面, 提问是否加入 PATH 环境变量和是否创建快捷方式. 我不建立快捷方式, 但是选择添加PATH环境变量.
之后一路下一步 (NEXT) 就完了.
然后, 在命令提示符 cmd, 或者在 powershell 中执行命令: cmake --version, 有正确反馈可以认为安装成功, 否则检查中间步骤问题.
Linux
第一种方法, 通过自带的软件管理器安装, 对于 Ubuntu, 执行 sudo aptitude install cmake; 对于 Arch, 执行 sudo pacman -S cmake;这种方法的优点在于, 不用管环境配置的问题, 但是版本问题影响不小, 对于 Ubuntu, 可能要及时更新系统到最新版本, 才能拿到最新的相关软件, 而更新系统又可能带来其他麻烦. 于是我使用第二种方法, 较为麻烦, 但是一劳永逸.
第二种方法: 来到下载页面, 找到对应的系统结构, 下载对应的 sh 文件:
来到下载目录(以Download为例), 为这个 sh 文件赋予运行权限 chmod +x ./cmake-*.sh
接下来, 第一种选择, 自行建立一个在根目录下的文件夹, 在这个文件夹下创建 cmake 文件夹. 第二种, 在 /opt 文件夹中创建 cmake 文件夹.
sudo mkdir /opt/cmake -p
# or
sudo mkdir /soft/cmake -p # 自行建立了一个名为 soft 的根目录文件夹
将你的 cmake 移到新建立的文件夹 cmake 中, 下述命令中, 我使用在根目录里面建立了 soft 文件夹, 并在其中建立了 cmake 文件夹的方式.
cd /soft/cmake
sudo mv $HOME/Download/cmake-*.sh ./
sudo bash ./cmake-*.sh
接下来安装程序提供一个协议说明, 按 q 键跳过. 然后大概是问: 你是不是想把这个软件安装到一个子目录(cmake-xxx)中? 也就是说, 安装之后的 cmake 软件在 /soft/cmake/cmake-xxx/ 里面, 他还说, saying no 会令软件安装到/soft/cmake 中. 所以这里给出否定输入. 让安装程序安装到 /soft/cmake 中.
安装之后, 您可能发现软件不能正常启动, 原因是没有配置环境变量. 我方式是, 创建一个位于 /etc/profile.d/ 文件夹中的以.sh为后缀的文件, 名字自定, 推荐使用 cmake.sh . 内容如下:
CMAKE_HOME=/soft/cmake
export PATH=${CMAKE_HOME}/bin:${PATH}
千万注意书写, 一旦这里产生错误, 比如PATH的内容写错了, 那很有可能, 你要恢复系统才能修正.
重新启动系统, 在终端中输入cmake --version 查看是否成功安装.
CMakeLists.txt 配置
主要内容
整个配置文件的内容逻辑主要就是:
- 设定配置项目使用的 cmake 软件的最低要求.
- (可选) 设定项目的编译器
- 设定项目名称; 使用的编程语言(C语言是 C, C++语言是CXX)(后者可选, 默认两者都有)
- 设定参与这个项目的源代码文件, 头文件和主文件
- 设定进行编译的结果文件名字, 参与编译的文件.
- (可选) 参与的动态链接库
- 参与这个项目编译结果的头文件基本路径.
- 其他命令等.
下面提供一个CMakeLists 配置文件的内容示例.
这是我的项目结构
.
├── CMakeLists.txt
├── main.cpp
├── src
│ ├── consvle
│ │ ├── macros
│ │ │ └── check_macr.h
│ │ └── typedefines
│ │ └── de_type.h
│ ├── de
│ │ ├── pubg.cpp
│ │ └── pubg.h
│ └── utls
│ ├── generate.cpp
│ ├── generate.h
│ ├── pointcloud
│ │ ├── view.cpp
│ │ └── view.h
我自己觉得这么安排挺舒服的. 也相信各位也有这样的感觉.
下面要写的就是这个 CMakeLists.txt.
1. 设置最低要求版本号.
上来第一句话, cmake_minimum_required(VERSION x.xx FATAL_ERROR) 其中, FATAL_ERROR 可选. 可以看到, 这句话要求了 CMake 的最低版本号, 今后你切换系统, 分享给别人等时, 如果对方的系统 CMake 版本低于 x.xx 就会报出警告, 说你这个不满足条件. 然后继续处理. 如果指定了 FATAL_ERROR, 则会报错并终止配置. 给出示例:
cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
2. 设置编译器(可选)
如果你的系统装了两个编译器. (gcc 和 clang), 如果不指定, 系统会决定用什么软件进行编译操作. 如果你有倾向性, 那么你需要指定一下, 注意, 在 windows 系统下, 默认路径分割是反斜线 '\', 这个符号各位清楚, 它也充当转义符号使用. CMake 也是这么干的. 所以请将路径中的反斜线分隔符换成正斜线.
e.g.
set(CMAKE_C_COMPILER C:/PATH/TO/THE/COMPILER.exe) # 设置 C 的
set(CMAKE_CXX_COMPILER C:/PATH/TO/THE/COMPILER.exe) # 设置 CPP 的
3. 设置项目名称, 编程语言(后者可选)
操作: project(NAME LANGUAGES C CXX) or project(NAME LANGUAGES C) or project(NAME LANGUAGES CXX)
如果不写 LANGUAGES 和后面的部分, 那就是 C 和 C++ 都会编译. 写哪个, CMake 就只会考虑使用哪个语言的编译器.
project 操作设定了变量 PROJECT_NAME 的值为 NAME
e.g.
project(de LANGUAGES CXX)
4. 设置语言标准, 设置是否强制遵从标准(均可选)
如果不指定, 则遵从编译器默认标准执行.
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
5. 设置参与编译的头文件, 源代码文件
file(GLOB TInc
"src/*.hpp"
"src/consvle/*.hpp"
"src/consvle/typedefines/*.hpp"
"src/consvle/macros/*.hpp"
"src/utls/*.hpp"
# "src/nude/*.hpp"
"src/de/*.hpp"
"src/utls/pointcloud/*.hpp"
)
file(GLOB Inc
"src/*.h"
"src/consvle/*.h"
"src/consvle/typedefines/*.h"
"src/consvle/macros/*.h"
"src/utls/*.h"
# "src/nude/*.h"
"src/de/*.h"
"src/utls/pointcloud/*.h"
)
file(GLOB Src
"src/*.cpp"
"src/consvle/*.cpp"
"src/consvle/typedefines/*.cpp"
"src/consvle/macros/*.cpp"
"src/utls/*.cpp"
# "src/nude/*.cpp"
"src/de/*.cpp"
"src/utls/pointcloud/*.cpp"
)
操作: file(类型 变量名 文件路径)
这个类型, 有两个常用: GLOB_RECURSE GLOB
变量名自己起
文件路径支持通配符
如果是GLOB_RECURSE那么就代表可以递归查询某文件夹中所有满足条件的文件. 本文没有使用这种方法. 考虑到以后可能的变动.
6. 设定进行编译的结果文件名字, 参与编译的文件.
命令: add_executable(OUTNAME FILES)
e.g.
add_executable(${PROJECT_NAME} ${Inc} ${Src} ${TInc} "main.cpp")
这条命令指定了, 输出内容和项目名称一致
7. (可选) 参与的动态链接库
target_link_libraries(OUTNAME LIBNAMES ... )
表示, 为这个 OUTNAME 制定名字为 LIBNAMES 的动态链接库(们)
- LIBNAMES 不只代表一个链接库.
- 其他命令等.
8. 设定头文件搜索基本地址
按照部分教材和网友的说明, # include <xxx.h> 是在系统路径中搜索, 而 # include "xxx.h" 是在当前文件夹和系统路径中搜索xxx.h. 这里做详细说明. 在 msys2 的终端环境中, 输入 echo | gcc -E -v - 可以看到 include <> 的所有搜索路径.

但是, 这个路径不是不变的, 通过一些参数补充. 在 cmake 中用命令
target_include_directories(<target> [SYSTEM] [AFTER|BEFORE]
<INTERFACE|PUBLIC|PRIVATE> [items1...]
[<INTERFACE|PUBLIC|PRIVATE> [items2...] ...])
事实上, system 我从没用过, AFTER BEFORE 我也没用过, 我们忽略. 接下来说一下 interface, public, private 的区别:
- interface: 自家不用, 给别人家用.
- private: 自家用, 别人家不能用
- public: 自家用, 别人家用.
您问我有没有自家不用, 别人家不用的? 那你写他干嘛?
这里的7和8的示例:
target_link_libraries(${PROJECT_NAME} ${LNK_LIB})
target_include_directories(${PROJECT_NAME} PRIVATE ${INC_DIR})
这么看可能整装一些, 我喜欢这么写, 在前面找地方另外定义 LNK_LIB和INC_DIR
set(LNK_LIB fmt::fmt)
set(INC_DIR "src")
9. 其他操作(可选)
考虑会有文件操作, 那就需要将相关文件放到构建文件夹中. 还有某些动态链接库, 只在你自己的某些环境中存着. 但是没在系统环境中存着; 或者你想把软件发过去, 对方没有相关的动态链接库. 这种情况, 你就需要把动态链接库也带着放到你的输出软件中. 你当然可以自己操作. 但是有方便的操作.
add_custom_target(copy_res
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/res $<TARGET_FILE_DIR:${PROJECT_NAME}>/res
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:fmt::fmt>
$<TARGET_FILE_DIR:${PROJECT_NAME}>
)
这个操作相当于我新定义了一个构建目标. 这个目标包含两个命令, 一个是赋值文件夹, 把项目根目录下的 res 文件夹复制到 输出文件夹中复制后的文件夹名字叫 res . 另一个是赋值不同文件, 如果文件发生变动, 则复制一次, 将 fmt::fmt 对应的文件赋值到输出文件夹中.
现在整体看一下配置:
cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
# set(CMAKE_CXX_COMPILER "clang++")
project(de LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
list(APPEND CMAKE_PREFIX_PATH "/some/paths/to/some/libs")
file(GLOB TInc
"src/*.hpp"
"src/consvle/*.hpp"
"src/consvle/typedefines/*.hpp"
"src/consvle/macros/*.hpp"
"src/utls/*.hpp"
# "src/nude/*.hpp"
"src/de/*.hpp"
"src/utls/pointcloud/*.hpp"
)
file(GLOB Inc
"src/*.h"
"src/consvle/*.h"
"src/consvle/typedefines/*.h"
"src/consvle/macros/*.h"
"src/utls/*.h"
# "src/nude/*.h"
"src/de/*.h"
"src/utls/pointcloud/*.h"
)
file(GLOB Src
"src/*.cpp"
"src/consvle/*.cpp"
"src/consvle/typedefines/*.cpp"
"src/consvle/macros/*.cpp"
"src/utls/*.cpp"
# "src/nude/*.cpp"
"src/de/*.cpp"
"src/utls/pointcloud/*.cpp"
)
find_package(fmt REQUIRED) # 能找到的前提是这个库在cmake的"搜索"路径中. 在开头使用 list 也是这个原因, 加入"搜索"路径.
set(LNK_LIB fmt::fmt)
set(INC_DIR "src")
add_executable(${PROJECT_NAME} ${Inc} ${Src} ${TInc} "main.cpp")
target_link_libraries(${PROJECT_NAME} ${LNK_LIB})
target_include_directories(${PROJECT_NAME} PRIVATE ${INC_DIR})
add_custom_target(copy_res
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/res $<TARGET_FILE_DIR:${PROJECT_NAME}>/res
COMMAND ${CMAKE_COMMAND} -E copy_if_different
$<TARGET_FILE:fmt::fmt>
$<TARGET_FILE_DIR:${PROJECT_NAME}>
)
这里给出不涉及链接库的一个版本, 这个版本的项目就无法使用 fmt 了.
cmake_minimum_required(VERSION 3.20 FATAL_ERROR)
project(de LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 23)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
file(GLOB TInc
"src/*.hpp"
"src/consvle/*.hpp"
"src/consvle/typedefines/*.hpp"
"src/consvle/macros/*.hpp"
"src/utls/*.hpp"
# "src/nude/*.hpp"
"src/de/*.hpp"
"src/utls/pointcloud/*.hpp"
)
file(GLOB Inc
"src/*.h"
"src/consvle/*.h"
"src/consvle/typedefines/*.h"
"src/consvle/macros/*.h"
"src/utls/*.h"
# "src/nude/*.h"
"src/de/*.h"
"src/utls/pointcloud/*.h"
)
file(GLOB Src
"src/*.cpp"
"src/consvle/*.cpp"
"src/consvle/typedefines/*.cpp"
"src/consvle/macros/*.cpp"
"src/utls/*.cpp"
# "src/nude/*.cpp"
"src/de/*.cpp"
"src/utls/pointcloud/*.cpp"
)
set(LNK_LIB )
set(INC_DIR "src")
add_executable(${PROJECT_NAME} ${Inc} ${Src} ${TInc} "main.cpp")
target_link_libraries(${PROJECT_NAME} ${LNK_LIB})
target_include_directories(${PROJECT_NAME} PRIVATE ${INC_DIR})
add_custom_target(copy_res
COMMAND ${CMAKE_COMMAND} -E copy_directory ${CMAKE_CURRENT_SOURCE_DIR}/res $<TARGET_FILE_DIR:${PROJECT_NAME}>/res
)
配置CMake项目
对于Linux的用户, 可以使用软件管理器安装 make 或 ninja. 十分方便.
对于 windows 用户, 请到 ninja-build 官网下载 ninja, 并将相关文件夹放到 PATH 环境变量里面. 这里不详述了.
在项目文件夹中编写程序, 然后设置好 CMake 的配置文件, 然后在项目根目录中打开 powershell 或 cmd 等, 创建文件夹用来放输出文件, 以build为例:
cd /path/to/proj
mkdir build
cd build
cmake .. -G Ninja # -G 表示 generator 构建软件, 前面说过 cmake 不能直接构建项目.
ninja # 此时已经生成了 build.ninja 文件了, ninja 就会按照这个文件构建项目
ninja copy_res # 用 ninja 构建 copy_res 的目标, 也就是让 ninja 执行赋值资源文件夹的命令.
以上操作完成后, 你会发现在 build 文件夹中多出了一个可执行文件, 这就是你编译的结果.
总结
本文介绍了 CMake 的安装过程和 CMakeLists.txt 配置文件的书写.
CMake 这个东西不是万能的, 对于简单的程序, 实际上没有使用它的必要, 使用 gcc 进行常规编译链接就行了. 只有较多的源代码文件需要进行项目的构建时候, CMake 才会体现出它的价值. 至于多大算大, 各位自行斟酌.
有很多内容本文没有说明, 比如 include, 比如find_package, install等. 也需要等到我对此有频繁需求, 自行研究明白以后, 再向大家补充关于 CMake 的更多使用方法.
非常感谢各位, 若有任何表述不清不当之处, 请评论区留言, 当我读到的时候, 会处理这些问题.

浙公网安备 33010602011771号