4、CMake入门:CMake安装、CMakeLists.txt编写、构建与安装、跨平台编译配置
说实话,我刚开始接触C语言项目时,最头疼的就是编译问题。
一个项目里几十个源文件,手动写Makefile?那简直是噩梦。后来我遇到了CMake,嗯,这玩意儿确实救了我一命。今天咱们就来聊聊CMake,这个C/C++项目构建的事实标准。
4.1 CMake是什么?为什么需要它?
CMake是个跨平台的构建工具。它不直接编译代码,而是生成Makefile或者Visual Studio的工程文件。说白了,它是个「构建系统的生成器」。
你想想看,Windows上用Visual Studio,Linux上用GCC,macOS上可能用Clang。每个平台的编译器、链接器、库路径都不一样。如果每个平台都写一套构建脚本,那维护成本就太高了。
CMake解决的就是这个问题——你写一份CMakeLists.txt,它帮你生成对应平台的构建文件。
核心思想: CMake把构建过程分成两步:
- 配置阶段(Configure): 读取CMakeLists.txt,检测编译器、库、头文件等环境信息
- 生成阶段(Generate): 根据配置结果,生成Makefile或工程文件
我在项目中遇到过最典型的场景:一个跨平台的网络库,需要在Linux、Windows、ARM嵌入式平台上编译。没有CMake的话,我得维护三套不同的构建脚本。有了CMake,一份CMakeLists.txt搞定所有平台。
4.2 CMake的安装
安装CMake其实很简单。不同平台的方法略有不同,我列个表给你看:
| 操作系统 | 安装方式 | 命令/步骤 |
|---|---|---|
| Ubuntu/Debian | apt | sudo apt install cmake |
| CentOS/RHEL | yum | sudo yum install cmake |
| macOS | Homebrew | brew install cmake |
| Windows | 安装包 | 从官网下载.msi安装,记得勾选「Add CMake to PATH」 |
我个人习惯用最新稳定版。你可以去 cmake.org 下载源码自己编译,但我建议直接用包管理器安装,省心。
小提示: 安装完后,在终端输入 cmake --version 检查是否成功。我见过有人装了半天,结果PATH没配好,命令行找不到cmake命令。
4.3 CMakeLists.txt 编写入门
CMakeLists.txt 是CMake的核心配置文件。每个项目至少有一个,放在项目根目录。
咱们从一个最简单的例子开始。假设项目结构是这样的:
my_project/
├── CMakeLists.txt
├── src/
│ ├── main.c
│ └── utils.c
└── include/
└── utils.h
对应的CMakeLists.txt长这样:
cmake_minimum_required(VERSION 3.10)
project(MyProject VERSION 1.0.0 LANGUAGES C)
# 设置C语言标准
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
# 添加头文件搜索路径
include_directories(${PROJECT_SOURCE_DIR}/include)
# 收集源文件
file(GLOB SOURCES "src/*.c")
# 生成可执行文件
add_executable(my_app ${SOURCES})
我来逐行解释一下:
cmake_minimum_required:指定CMake的最低版本。低于这个版本会报错。project:定义项目名称、版本号、使用的语言。这里我指定了C语言。set:设置变量。CMAKE_C_STANDARD指定C语言标准为C11。include_directories:添加头文件搜索路径。${PROJECT_SOURCE_DIR}是CMake内置变量,指向项目根目录。file(GLOB ...):用通配符收集源文件。注意:我建议小项目用这个,大项目还是手动列出源文件更可靠。add_executable:告诉CMake要生成一个可执行文件,名字叫my_app。
注意: file(GLOB ...) 有个坑——如果你新增了源文件,CMake不会自动重新扫描。需要手动重新运行cmake命令。我曾经因为这个浪费了半天时间,新加的文件死活编译不进去。
4.4 构建与安装
写好CMakeLists.txt后,怎么构建呢?我推荐用「外部构建」方式,也就是在项目目录外建一个build文件夹:
mkdir build
cd build
cmake ..
make
为什么要用外部构建?因为CMake会在build目录里生成一堆中间文件(Makefile、CMakeCache.txt等)。如果直接在源码目录里构建,这些文件会污染源码目录。我刚开始用CMake时就在源码目录里直接跑cmake,结果目录乱成一锅粥。
构建完成后,生成的可执行文件在build目录里。如果你想安装到系统目录,可以执行:
sudo make install
但安装路径怎么控制呢?CMake提供了 CMAKE_INSTALL_PREFIX 变量:
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make
sudo make install
在CMakeLists.txt里,你需要用 install() 命令来指定安装规则:
# 安装可执行文件到 bin 目录
install(TARGETS my_app DESTINATION bin)
# 安装头文件到 include 目录
install(DIRECTORY include/ DESTINATION include)
# 安装库文件到 lib 目录(如果有库的话)
install(TARGETS my_lib LIBRARY DESTINATION lib)
我个人习惯把安装路径设为 /usr/local,这样不会和系统自带的软件冲突。
4.5 跨平台编译配置
跨平台是CMake的强项。我来说说常见的跨平台场景。
4.5.1 检测操作系统
CMake提供了 CMAKE_SYSTEM_NAME 变量,可以判断当前是什么系统:
if(CMAKE_SYSTEM_NAME STREQUAL "Linux")
message("当前是Linux系统")
# Linux特有的配置
elseif(CMAKE_SYSTEM_NAME STREQUAL "Windows")
message("当前是Windows系统")
# Windows特有的配置
elseif(CMAKE_SYSTEM_NAME STREQUAL "Darwin")
message("当前是macOS系统")
endif()
4.5.2 检测编译器
有时候需要根据编译器做不同处理。比如GCC和MSVC的编译选项不一样:
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
# GCC编译器
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
# Visual Studio编译器
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W4")
elseif(CMAKE_C_COMPILER_ID STREQUAL "Clang")
# Clang编译器
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra -Wpedantic")
endif()
我在项目中遇到过一个问题:在Linux上用GCC编译好好的代码,到Windows上用MSVC就报错。原因是MSVC对C99的支持不太一样。后来我用CMake检测编译器,针对MSVC加了一些兼容性宏定义,问题就解决了。
4.5.3 交叉编译
交叉编译是指在一个平台上编译出另一个平台能运行的程序。比如在x86的Linux上编译ARM架构的程序。
交叉编译需要写一个工具链文件(toolchain.cmake),告诉CMake编译器、链接器在哪:
# arm-toolchain.cmake
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR arm)
set(CMAKE_C_COMPILER arm-linux-gnueabihf-gcc)
set(CMAKE_CXX_COMPILER arm-linux-gnueabihf-g++)
set(CMAKE_FIND_ROOT_PATH /usr/arm-linux-gnueabihf)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
使用时,通过 -DCMAKE_TOOLCHAIN_FILE 指定工具链文件:
cmake .. -DCMAKE_TOOLCHAIN_FILE=arm-toolchain.cmake
经验之谈: 交叉编译最容易出问题的是库依赖。目标平台上的库路径、版本都要和工具链匹配。我建议先编译一个简单的hello world程序,确认工具链没问题,再编译复杂项目。
4.6 CMake知识体系总览
下面这张图总结了CMake的核心知识结构,你可以对照着回顾一下:
4.7 一些实用技巧
最后,分享几个我平时常用的CMake技巧:
- 打印变量调试: 用
message(STATUS "变量值: ${VAR}")可以打印变量内容,方便调试。 - 设置输出目录: 用
set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin)可以把可执行文件统一放到bin目录。 - 条件编译: 用
option()定义开关,比如option(ENABLE_DEBUG "启用调试输出" OFF),然后在代码里用#ifdef ENABLE_DEBUG控制。 - 查找库: 用
find_package()自动查找系统已安装的库,比如find_package(OpenSSL REQUIRED)。
避坑指南: 我曾经在Windows上遇到一个坑——CMake生成的Visual Studio工程默认是32位的。如果想编译64位程序,需要在cmake命令里指定:cmake .. -A x64。这个参数藏得比较深,我当初找了半天才找到。
好了,CMake入门就聊到这里。记住一句话:CMake不是万能的,但没有CMake是万万不能的。尤其是跨平台项目,CMake能帮你省下大量时间。动手写一个CMakeLists.txt试试吧,你会发现它其实没那么复杂。