如何编写一个CMakelists文件

一、前言

先和大家讲一讲CMake的作用是什么。一个很形象的比喻,假如完成一个项目就好像一个厨师在做菜,我们平时的c/c++代码中写的是如何制作这份美味的大餐,要加什么菜、什么时候加、每一份要加多少;而我们的CMake就是为了告诉厨师,A类菜放在哪里,B类菜放在哪里.....,这样,使用CMakeLists将整个项目的源文件、头文件、可执行文件等等关联在一起,就变成一个完整的项目。

之前有学习过makefile的同学很容易理解上面提到的这一点,那么我们为什么在复杂的项目中要使用CMake而不是makefile呢?在makefile中我们需要手动的设置每一个编译目标,编译方式,编译依赖的文件等等,这些依赖关系一旦复杂,就很有可能出错。想编写好一份复杂且高效的makefile文件是极其困难的,所以CMake出现了,我们编写CMake之后其会自动生成makefile去工作,所以makefile更偏底层,我们一般使用CMake就好,这是便于我们构建项目的工具,我们应该抱着积极而不是畏惧的心态去学习(如果接受不了的话自己用makefile写一个复杂项目的构建,你会发现CMake还是很美好的)

二、CMake的基础配置

1.告诉CMake至少需要哪个版本的CMake才能正常工作

cpp 复制代码
cmake_minimum_required(VERSION 3.16)

2.定义项目名称(MiniDeviceProxy)、项目版本(0.1.0)和使用的语言(CXX,也就是C++)

cpp 复制代码
project(MiniDeviceProxy VERSION 0.1.0 LANGUAGES CXX)

3.设置一个变量

cpp 复制代码
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

这里设置的变量都是CMAKE_开头(包括project命令自动设置的变量),这类变量都是CMake的内置变量,正是通过修改这些变量的值来配置CMake构建的行为。

4.通过命令target_compile_options命令可以为所有编译器配置编译选项(同时对多个编译器生效); 通过设置变量CMAKE_C_FLAGS可以配置c编译器的编译选项; 而设置变量CMAKE_CXX_FLAGS可配置针对c++编译器的编译选项。 比如:

cpp 复制代码
target_compile_options(mdp_logger PRIVATE
    -Wall -Wextra -pedantic -Werror
)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -pipe -std=c99")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -pipe -std=c++11")

5.通过设置变量CMAKE_BUILD_TYPE可以设置编译类型,可用的类型有:Debug、Release、RelWithDebInfoMinSizeRel等,比如:

复制代码
set(CMAKE_BUILD_TYPE Debug)

当然,更好的方式应该是在执行cmake命令的时候通过参数-D指定:

复制代码
cmake -B build -DCMAKE_BUILD_TYPE=Debug

如果设置编译类型为Debug,那么对于c编译器,CMake会检查是否有针对此编译类型的编译选项CMAKE_C_FLAGS_DEBUG,如果有,则将它的配置内容加到CMAKE_C_FLAGS中。

可以针对不同的编译类型设置不同的编译选项,比如对于Debug版本,开启调试信息,不进行代码优化:

复制代码
set(CMAKE_C_FLAGS_DEBUG "${CMAKE_C_FLAGS_DEBUG} -g -O0")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -g -O0")

对于Release版本,不包含调试信息,优化等级设置为2:

复制代码
set(CMAKE_C_FLAGS_RELEASE "${CMAKE_C_FLAGS_RELEASE} -O2")
set(CMAKE_CXX_FLAGS_RELEASE "${CMAKE_CXX_FLAGS_RELEASE} -O2")

这里的o1、o2指的是编译器优化程度,学习过makefile的同学可能比较熟悉,这里不再赘述

6.通过target_compile_definitions可以添加全局的宏定义,后续代码可根据此宏区分代码

cpp 复制代码
target_compile_definitions(mdp_logger PRIVATE DEBUG)

7.通过target_include_directories添加头文件的搜索目录

cpp 复制代码
target_include_directories(mdp_logger PUBLIC
    ${CMAKE_CURRENT_SOURCE_DIR}
)

这时面向target的写法,整个架构中,只有对应target的部分才能阅读这个命令添加的头文件

三、目标编译

一般来说,编译目标有静态库、动态库和可执行文件,这时编写cmakelists主要分为两步:

1.编译:确定编译目标所需要的源文件

2.链接:确定链接时所需要额外的依赖的库

我们先介绍两个概念:

  • 静态库:编译时把代码"拷贝进"最终程序
  • 动态库:程序运行时再去"加载"那份库文件

你写了很多 .cc 文件,里面有一些通用功能,比如:

  • 日志
  • 配置读取
  • 网络通信
  • 字符串处理

这些功能不一定都直接写进 main.cc,通常会先编译成"可复用的代码包",这个代码包就是"库"。

也就是说:

  • 可执行文件:最终运行的程序,比如 mini_proxyd
  • 库:给程序使用的一组已经编译好的功能代码

假如我们有一个a.ccb.cc、c.cc文件,可以先将其编译为a.o、b.o、c.o文件,然后将这三个.o文件打包为统一的静态库abc.a。在linux中.a后缀的就是静态库。当生成最终的可执行文件时,链接器就会将目标可执行文件使用的代码,从静态库中拷贝到主目标。

与上面相似,将众多.o打包成.so文件,就叫做动态库。主程序启动时,会去找:abc.so,然后由系统动态加载器把它加载进进程空间。

静态库优点

  • 部署简单
  • 运行时依赖少
  • 对小项目和工具型程序很友好
  • 不容易出现"找不到库"的问题

静态库缺点

  • 最终程序更大
  • 多个程序如果都用同一份库,会各自拷一份代码
  • 更新库后通常要重新链接程序

动态库优点

  • 多个程序可以共享同一份库
  • 可执行文件更小
  • 库单独升级更方便

动态库缺点

  • 部署更复杂
  • 运行时可能遇到找不到 .so
  • 版本兼容问题更多
  • 对初学者更容易踩坑

步骤1.编译静态库:

cpp 复制代码
file(GLOB_RECURSE MATH_LIB_SRC src/c/math/*.c)

add_library(math STATIC ${MATH_LIB_SRC})

src/c/math/ 下面符合条件的 .c 文件都收集起来,放进变量 MATH_LIB_SRC,然后用这些源文件生成一个叫 math 的静态库。

file()命令有许多用法:读取文件、写文件、搜索文件、拷贝文件。这里用到的是搜索文件,GLOB_RECURSE就是file的一种模式,GLOB:按模式匹配文件,RECURSE:递归进入子目录继续找。使用GLOB_RECURSE虽然方便,但是有一个问题,不会动态感知新增的文件,假如你后续还向该目录添加了新的.cc文件,CMAKE不一定能感知到,所以很多项目还是显式的列出所有源代码文件名称。

第二行的意思是把MATH_LIB_SEC中的文件编译成一个叫math的静态库

步骤2.编译可执行文件:

通过add_executable来向构建系统中添加一个可执行构建目标,也需要指定编译需要的源文件。但是对于可执行文件来说,有时还需要依赖其他库,则需要target_link_libraries来声明构建此可执行目标需要链接的库

cpp 复制代码
add_executable(demo src/c/main.c)
target_link_libraries(demo math)

第一行说明demo需要的源文件(也可以是多个)

四、安装和打包

  • 安装 install:把你编译好的产物按规则放到目标目录里
  • 打包 package:把这些产物进一步整理成一个可分发的安装包或压缩包

进行完编译之后,通常你得到的是:

  • 可执行文件
  • 静态库
  • 动态库
  • 测试程序

这些文件都在 build/ 目录里,但是注意:build/ 目录只是构建输出目录,不是正式安装目录。

安装就是把build中的产物,按照部署规则重新移动到其他目录下,按照你写的安装规则,把构建产物拷贝到规范目录。这一步已经接近真实的部署环境,比如将目录分为lib、bin等目录,将对应的产物放入其中,使其的分布符合最终的部署环境。

安装干的事其实就两件:

  1. 筛选:决定哪些东西值得交付。

  2. 摆放:决定这些东西交付时放哪。所以它解决的问题是把"构建产物"整理成"可部署产物"。

打包就是在安装的基础上,将安装后的产物进一步打包为固定格式,比如.zip/.deb等等,生成一个可以传输的文件

对于安装来说:

1.通过install说明需要安装的内容及目标路径

2.通过设置CMAKE_INSTALL_PREFIX设置安装的路径

3.使用cmake --install --prefix <install-path>覆盖指定安装路径。

复制代码
install(TARGETS math demo
        RUNTIME DESTINATION bin
        LIBRARY DESTINATION lib
        ARCHIVE DESTINATION lib)

这里通过TARGETS参数指定需要安装的目标列表;参数RUNTIME DESTINATIONLIBRARY DESTINATIONARCHIVE DESTINATION分别指定可执行文件、库文件、归档文件分别应该安装到安装目录下个哪个子目录。

如果指定CMAKE_INSTALL_PREFIX/usr/local,那么math库将会被安装到路径/usr/local/lib/目录下;而demo可执行文件则在/usr/local/bin目录下。

对于打包来说:

要使用打包功能,需要执行include(CPack)启用相关的功能,在执行构建编译之后使用cpack命令行工具进行打包安装;对于make工具,也可以使用命令make package

打包的内容就是install命令安装的内容,关键需要设置的变量有:

CPACK_GENERATOR 打包使用的压缩工具,比如"ZIP"
CPACK_OUTPUT_FILE_PREFIX 打包安装的路径前缀
CPACK_INSTALL_PREFIX 打包压缩包的内部目录前缀
CPACK_PACKAGE_FILE_NAME 打包压缩包的名称,由CPACK_PACKAGE_NAME、CPACK_PACKAGE_VERSION、CPACK_SYSTEM_NAME三部分构成

比如:

复制代码
include(CPack)
set(CPACK_GENERATOR "ZIP")
set(CPACK_PACKAGE_NAME "CMakeTemplate")
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
相关推荐
踮起脚看烟花2 小时前
chapter10_泛型算法
c++·算法
山栀shanzhi2 小时前
C++四大常见排序对比
c++·算法·排序算法
Ivanqhz2 小时前
LLVM IR 转 SMT公式
java·开发语言
云栖梦泽2 小时前
Linux内核与驱动:8.ioctl驱动基础
linux·c++
小红的布丁2 小时前
Reactor 模型详解:单 Reactor、主从 Reactor 与 Netty 思想
android·java·开发语言
被摘下的星星2 小时前
Java的类加载
java·开发语言
云栖梦泽2 小时前
Linux内核与驱动:7.从应用层 lseek() 到驱动层 .llseek,Linux 字符设备偏移控制详解
linux·c++
skilllite作者2 小时前
SkillLite 多入口架构实战:CLI / Python SDK / MCP / Desktop / Swarm 一页理清
开发语言·人工智能·python·安全·架构·rust·agentskills
秋月的私语2 小时前
遥感影像拼接线优化工具:基于Qt+GDAL+OpenCV的从零到一实践
开发语言·qt·opencv