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的核心知识结构,你可以对照着回顾一下:

CMake 知识体系 CMake 安装 apt/brew/msi 源码编译 CMakeLists.txt 编写 project/add_executable include_directories 构建与安装 外部构建 (mkdir build) make install 跨平台编译 CMAKE_SYSTEM_NAME 交叉编译 (toolchain)

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试试吧,你会发现它其实没那么复杂。


公众号:蓝海资料掘金营,微信deep3321