4. Makefile基础:构建自动化第一步

说到嵌入式交叉编译,Makefile 是绕不开的话题。我个人觉得,Makefile 就像项目的「施工图纸」—— 它告诉编译器怎么把源代码变成可执行文件。很多初学者觉得 Makefile 难,其实说白了,它就是个带依赖检查的批处理脚本。

我记得刚入行时,有个项目需要同时编译 ARM 和 x86 两个版本。手动敲命令敲到崩溃,后来花了一天时间写了个 Makefile,从此一劳永逸。嗯,今天我就把这些年积累的 Makefile 经验分享给你。

4.1 Makefile 基本语法

Makefile 的核心结构其实很简单:

目标(target): 依赖(prerequisites)
    命令(command)

目标通常是你要生成的文件名,依赖是生成目标所需的文件,命令则是具体的编译动作。注意,命令前面必须是一个 Tab 键,不能是空格。这个坑我踩过好几次,每次都是编译报错才想起来检查缩进。

来看个最简单的例子:

hello: hello.c
    gcc -o hello hello.c

执行 make hello,Make 会检查 hello.c 是否比 hello 新,如果是,就执行 gcc 命令重新编译。

注意:命令前的 Tab 是硬性要求。我曾经在代码审查时看到同事用空格代替,结果 make 报错「missing separator」。这个错误信息很常见,遇到时先检查缩进。

4.2 变量定义与使用

写 Makefile 时,最怕的就是硬编码。比如编译器路径、编译选项这些,如果写死在规则里,换平台就得改整个文件。这时候变量就派上用场了。

变量定义很简单:

CC = arm-linux-gnueabihf-gcc
CFLAGS = -Wall -O2 -march=armv7-a
TARGET = hello

使用变量时用 $(变量名)${变量名}

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

我个人习惯用 $() 形式,因为和 Shell 的 ${} 容易混淆的场景更少。变量名建议全大写,这样一眼就能看出是 Makefile 变量。

小技巧:变量可以延迟展开,也可以立即展开。用 = 是延迟展开(使用时才计算值),用 := 是立即展开(定义时就计算)。交叉编译时我常用 :=,避免变量值被后续修改影响。

4.3 自动变量

自动变量是 Makefile 里的「语法糖」,能省不少事。我最常用的几个:

自动变量 含义 示例
$@ 目标文件名 在规则中代表当前目标
$< 第一个依赖文件 常用于编译单个源文件
$^ 所有依赖文件 链接时列出所有 .o 文件
$* 目标的主文件名(不含后缀) 模式规则中常用

举个例子,你想想看,如果没有自动变量,编译多个文件时得写多少重复代码:

# 不用自动变量
hello.o: hello.c
    arm-linux-gnueabihf-gcc -c hello.c -o hello.o

main.o: main.c
    arm-linux-gnueabihf-gcc -c main.c -o main.o

# 用自动变量
%.o: %.c
    $(CC) $(CFLAGS) -c $< -o $@

看到区别了吧?自动变量让规则变得通用。我在项目中经常用 $@$< 组合,几乎成了肌肉记忆。

4.4 模式规则

模式规则是 Makefile 的「模板引擎」。它用 % 通配符匹配文件名,让一条规则适用于所有同类文件。

刚才的例子就是模式规则:

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

这里的 % 匹配任意字符串。比如要编译 hello.c,% 就匹配 hello,然后 $< 变成 hello.c,$@ 变成 hello.o。

我曾经维护过一个项目,里面有 200 多个 .c 文件。如果没有模式规则,Makefile 得写 200 多条编译规则,想想就头皮发麻。模式规则一条搞定。

实战经验:交叉编译时,模式规则特别适合处理不同架构的编译。比如 ARM 和 x86 共用一套规则,只需改变 CC 和 CFLAGS 变量即可。

4.5 伪目标

伪目标不是真正的文件,而是 Makefile 里的「命令别名」。最常见的伪目标是 clean

.PHONY: clean
clean:
    rm -f *.o $(TARGET)

为什么要加 .PHONY?因为如果当前目录下恰好有个叫 clean 的文件,Make 会认为 clean 已经是最新的,不会执行命令。声明为伪目标后,Make 就不检查文件是否存在,直接执行命令。

我常用的伪目标还有:

  • all:默认目标,编译所有内容
  • install:安装到指定目录
  • distclean:彻底清理,包括配置文件
  • help:显示帮助信息

举个例子:

.PHONY: all clean install

all: $(TARGET)

install: $(TARGET)
    cp $(TARGET) /usr/local/bin/

clean:
    rm -f *.o $(TARGET)

避坑指南:我曾经在一个项目中忘记加 .PHONY,结果同事在源码目录创建了一个叫 clean 的空文件,导致 make clean 永远不执行。排查了半天才发现是这个问题。所以,所有非文件目标都建议加上 .PHONY。

4.6 常见函数

Makefile 的函数让自动化更进一步。我用的最多的是 wildcardpatsubst

4.6.1 wildcard:文件通配

wildcard 用来获取匹配模式的文件列表:

SRC = $(wildcard *.c)

这会把当前目录下所有 .c 文件赋值给 SRC 变量。如果目录下有 hello.c、main.c、utils.c,那么 SRC 就是 hello.c main.c utils.c

我经常这样用:

SRC = $(wildcard src/*.c)
OBJ = $(SRC:.c=.o)

这样就不用手动列出每个源文件了。项目新增文件时,Makefile 自动适配。

4.6.2 patsubst:模式替换

patsubst 用来做字符串的模式替换,比 $(SRC:.c=.o) 更灵活:

OBJ = $(patsubst %.c, %.o, $(SRC))

这会把 SRC 中所有 .c 结尾的字符串替换成 .o。和 $(SRC:.c=.o) 效果一样,但 patsubst 支持更复杂的模式。

实际项目中我这样组合使用:

SRC_DIR = src
OBJ_DIR = obj

SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
    $(CC) $(CFLAGS) -c $< -o $@

这样源文件和目标文件分开放,目录结构清晰。我在嵌入式项目中一直用这个模式,编译 ARM 版本时只需改 CC 和 CFLAGS。

个人习惯:我通常把源文件列表、目标文件列表、头文件路径都定义成变量,放在 Makefile 开头。这样后续维护时,改一处就行,不用在规则里到处找。

4.7 综合示例:嵌入式交叉编译 Makefile

最后,给你看一个我实际项目中用的 Makefile 模板:

# 交叉编译器
CROSS_COMPILE = arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
LD = $(CROSS_COMPILE)ld
AR = $(CROSS_COMPILE)ar

# 编译选项
CFLAGS = -Wall -O2 -march=armv7-a -mfpu=neon
LDFLAGS = -lm -lpthread

# 目录结构
SRC_DIR = src
INC_DIR = inc
OBJ_DIR = obj
BIN_DIR = bin

# 源文件
SRC = $(wildcard $(SRC_DIR)/*.c)
OBJ = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRC))
TARGET = $(BIN_DIR)/app

# 头文件路径
CFLAGS += -I$(INC_DIR)

# 默认目标
.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJ) | $(BIN_DIR)
    $(CC) $(LDFLAGS) -o $@ $^

$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c | $(OBJ_DIR)
    $(CC) $(CFLAGS) -c $< -o $@

$(OBJ_DIR):
    mkdir -p $@

$(BIN_DIR):
    mkdir -p $@

clean:
    rm -rf $(OBJ_DIR) $(BIN_DIR)

这个 Makefile 包含了今天讲的所有知识点:变量、自动变量、模式规则、伪目标、wildcard 和 patsubst。你拿去改改交叉编译器路径就能用。

嗯,Makefile 的基础就这些。记住一点:写 Makefile 的目的是「少写命令」,而不是「炫技」。保持简单清晰,团队里其他人也能看懂,这才是好 Makefile。