第25章:Makefile与CMake对比:语法对比、可维护性对比、跨平台性对比、适用场景

做嵌入式开发这些年,我一直在跟构建系统打交道。说实话,Makefile和CMake这两个工具,几乎每个嵌入式工程师都绕不开。很多新手会问:「到底学哪个好?」我的回答是:两个都得会,但得知道什么时候用哪个。

这一章,咱们就从语法、可维护性、跨平台性、适用场景四个维度,把这两个工具掰开揉碎对比一下。嗯,我会结合我踩过的坑来讲,希望能帮你少走弯路。

一、语法对比:一个像C,一个像脚本

先看个最简单的例子——编译一个hello.c文件。

Makefile写法

CC = gcc
CFLAGS = -Wall -O2
TARGET = hello

$(TARGET): hello.c
    $(CC) $(CFLAGS) -o $@ $<

clean:
    rm -f $(TARGET)

CMake写法

cmake_minimum_required(VERSION 3.10)
project(hello)
add_executable(hello hello.c)

看到区别了吗?Makefile用的是制表符缩进来定义规则,CMake用的是函数调用风格。我个人觉得,CMake的语法更接近高级语言,可读性更好。

核心差异点:

  • 变量定义:Makefile用 VAR = value,CMake用 set(VAR value)
  • 条件判断:Makefile用 ifeq/ifneq,CMake用 if/else/endif
  • 循环:Makefile用 $(foreach ...),CMake用 foreach()
  • 函数:Makefile用 $(call ...),CMake用 function()

我的经验:Makefile的语法太「隐式」了。比如那个$@$<,新手看了直接懵。CMake的add_executable一眼就知道是干啥的。所以团队协作时,我倾向于用CMake。

二、可维护性对比:谁更抗造?

项目规模一大,构建系统的可维护性就成了关键。我曾经接手过一个有2000行Makefile的老项目……嗯,那感觉就像在拆炸弹。

Makefile的痛点

  • 隐式规则太多:自动推导规则虽然方便,但出了问题很难排查
  • 变量作用域混乱=:=?=+=,四种赋值方式,稍不注意就出bug
  • 没有模块化机制:只能靠include,但include的文件多了,依赖关系一团糟
  • 调试困难make -d输出的信息量太大,根本看不完

CMake的优势

  • 清晰的目录结构:每个子目录一个CMakeLists.txt,层次分明
  • 目标导向:所有操作围绕target(可执行文件、库)展开
  • 内置模块find_packageFetchContent等,省去手写查找逻辑
  • 生成器表达式$<CONFIG:Debug>这种写法,比Makefile的条件判断优雅多了

避坑指南:我曾经在一个项目里用Makefile管理30多个子模块,结果每次改依赖关系都要改半天。后来换成CMake,用target_link_libraries自动传递依赖,世界清净了。

三、跨平台性对比:一次编写,到处编译?

嵌入式开发最头疼的就是跨平台。ARM、x86、RISC-V,Linux、Windows、RTOS……构建系统能不能搞定这些,直接决定了你的工作效率。

对比维度 Makefile CMake
原生支持 Linux/Unix原生,Windows需额外工具 全平台原生支持
交叉编译 手动设置CC/AR/LD等变量 通过toolchain文件统一配置
IDE集成 需手动配置 可生成VS、Xcode、Eclipse等工程
包管理 无内置支持 FetchContent、CPack、vcpkg
测试支持 需手写test目标 CTest集成,一键运行

说白了,Makefile的跨平台能力完全取决于你写Makefile的水平。而CMake把跨平台的复杂性封装起来了——你只需要写一份CMakeLists.txt,它就能在Windows上生成Visual Studio工程,在Linux上生成Makefile,在macOS上生成Xcode工程。

交叉编译实战对比:

Makefile方式:

export CC=arm-none-eabi-gcc
export AR=arm-none-eabi-ar
export LD=arm-none-eabi-ld
make

CMake方式:

# toolchain.cmake
set(CMAKE_C_COMPILER arm-none-eabi-gcc)
set(CMAKE_CXX_COMPILER arm-none-eabi-g++)
# 使用
cmake -DCMAKE_TOOLCHAIN_FILE=toolchain.cmake ..

我个人觉得,CMake的toolchain文件机制更优雅。你想想看,把交叉编译的配置单独抽出来,换一个芯片平台就换一个toolchain文件,多清爽。

四、适用场景:什么时候用哪个?

没有银弹。Makefile和CMake各有各的用武之地。我根据实际项目经验,给你画了张决策图。

构建系统选型决策图 项目规模多大? 小型项目(单文件/少文件) 建议:Makefile 中型项目(多模块) 建议:CMake 大型项目(跨平台) 建议:CMake 优点:零依赖、简单直接 缺点:跨平台需手动处理 优点:模块化、可维护性好 缺点:学习曲线略陡 优点:跨平台、IDE集成 缺点:构建速度略慢 核心原则:小项目用Makefile,大项目用CMake

具体场景建议

  1. 裸机MCU项目(STM32、ESP32等):我个人习惯用Makefile。因为这类项目通常文件不多,而且需要精细控制编译选项。Makefile的灵活性在这里是优势。
  2. Linux应用开发:强烈推荐CMake。Linux上的库依赖管理、多目录结构,CMake处理起来得心应手。
  3. 跨平台库开发:必须用CMake。你想想看,一个库要同时支持Windows、Linux、macOS,用Makefile写条件判断会写到崩溃。
  4. 快速原型验证:Makefile就够了。我经常在开发板上写个简单的Makefile,三五行就搞定编译。
  5. 大型团队协作:CMake是唯一选择。它的target依赖图、安装规则、测试集成,都是为团队协作设计的。

我的建议:别把Makefile和CMake对立起来。实际项目中,我经常在CMake里调用Makefile——比如用CMake管理整体项目结构,但某个底层驱动库仍然用Makefile编译。工具是死的,人是活的。

五、总结:没有最好,只有最合适

写到这里,我想起刚入行时的一个项目。当时我用Makefile管理一个包含50多个源文件的嵌入式Linux项目,每次改依赖关系都要小心翼翼。后来换成CMake,用target_sourcestarget_include_directories,代码结构一目了然。

但反过来,我也见过有人用CMake写一个只有两个文件的hello world项目——嗯,杀鸡用牛刀了。

所以我的建议是:

  • Makefile:适合小项目、快速原型、需要精细控制编译选项的场景
  • CMake:适合中大型项目、跨平台项目、团队协作的场景

两个都学会,你才能在面对不同项目时,选出最合适的工具。毕竟,嵌入式工程师的核心竞争力不是会用某个工具,而是知道什么时候用什么工具。


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