4. Makefile基础
说实话,很多做嵌入式Linux的朋友,一开始都被Makefile劝退过。我当年也是。看着那一堆冒号、美元符号、Tab键,心里直犯嘀咕:这玩意儿到底在干嘛?
但后来我明白了,Makefile其实就是一套“自动化编译说明书”。你告诉它:哪些文件需要编译、用什么工具、依赖什么头文件。剩下的脏活累活,它全包了。
这一章,咱们就把Makefile的底裤扒干净。从最基础的规则讲起,再到变量、函数,最后搞一个能自动编译的工程。嗯,保证你学完就能上手。
4.1 规则:Makefile的灵魂
Makefile的核心就一句话:目标依赖于前提,然后执行命令。写成公式就是:
目标: 前提
命令
举个例子。假设你有一个 main.c,想编译成 main.o:
main.o: main.c
gcc -c main.c -o main.o
这里 main.o 是目标,main.c 是前提。当 main.c 比 main.o 新时,make就会执行下面的命令。
关键点:命令前面必须是一个Tab键,不能是空格。我见过太多新手在这里栽跟头了。你敲8个空格都不行,必须是Tab。
再复杂一点,多文件依赖怎么写?
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c
gcc -c main.c -o main.o
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
你看,make会递归检查依赖。如果 utils.h 改了,utils.o 就会重新编译,然后 app 也会重新链接。这就是Makefile的“增量编译”精髓——只编译改动的文件,省时间。
4.2 变量:别写死,用变量
我刚开始写Makefile时,所有路径、编译器、参数都写死。后来项目大了,改一个编译器就得改几十处。那叫一个酸爽。
所以,用变量吧。Makefile的变量用 $() 或 ${} 引用:
CC = gcc
CFLAGS = -Wall -O2
TARGET = app
$(TARGET): main.o utils.o
$(CC) main.o utils.o -o $(TARGET)
main.o: main.c
$(CC) $(CFLAGS) -c main.c -o main.o
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c utils.c -o utils.o
现在想换编译器?改一行 CC = arm-linux-gnueabihf-gcc 就行。想加调试信息?在 CFLAGS 后面加个 -g。方便吧?
我的习惯:变量名全大写,用下划线分隔。比如 SRC_DIR、INC_DIR。这样一眼就能看出是变量,不是目标名。
常用的内置变量也记一下:
| 变量 | 含义 |
|---|---|
$@ |
目标文件名 |
$< |
第一个前提文件 |
$^ |
所有前提文件(去重) |
$? |
比目标新的前提文件 |
用这些变量,上面的例子可以简化成:
$(TARGET): main.o utils.o
$(CC) $^ -o $@
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
看到那个 %.o: %.c 了吗?这叫模式规则,后面会细说。但你现在应该能感受到:变量让Makefile变得灵活多了。
4.3 函数:让Makefile更智能
变量解决了“改一处”的问题,但还不够。比如,你有一个目录 src/,里面放了20个.c文件。难道要一个一个写进Makefile?
这时候就该函数上场了。Makefile内置了很多函数,我常用的就这几个:
$(wildcard 模式):查找匹配的文件$(patsubst 原模式, 新模式, 文本):替换后缀$(foreach 变量, 列表, 表达式):循环处理
看个实际例子:
SRC_DIR = src
OBJ_DIR = obj
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))
$(TARGET): $(OBJS)
$(CC) $^ -o $@
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c
$(CC) $(CFLAGS) -c $< -o $@
这段代码干了什么?
wildcard自动找到src/下所有.c文件patsubst把src/xxx.c变成obj/xxx.o- 模式规则
$(OBJ_DIR)/%.o: $(SRC_DIR)/%.c自动匹配每个文件
现在你加一个新 .c 文件到 src/ 目录,Makefile 不用改一行。这就是自动化编译的威力。
我曾经踩过的坑:wildcard 匹配不到文件时,不会报错,而是返回空字符串。然后 OBJS 就空了,链接时啥也没有。所以建议加个检查:
ifeq ($(strip $(SRCS)),)
$(error 没有找到源文件!)
endif
4.4 自动化编译:从手动到自动
好了,前面都是基础。现在咱们把前面学的串起来,搞一个真正能用的自动化编译系统。
先画个图,看看整体流程:
流程其实不复杂:源文件和头文件经过编译生成目标文件(.o),再链接成可执行文件。头文件变了,相关的 .o 就得重编。这就是Makefile帮我们自动做的事情。
下面是一个完整的自动化Makefile模板,我项目里经常这么用:
# 编译器与参数
CC = gcc
CFLAGS = -Wall -O2 -Iinclude
LDFLAGS =
# 目录
SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
BIN_DIR = bin
# 自动查找源文件
SRCS = $(wildcard $(SRC_DIR)/*.c)
OBJS = $(patsubst $(SRC_DIR)/%.c, $(OBJ_DIR)/%.o, $(SRCS))
TARGET = $(BIN_DIR)/app
# 默认目标
all: $(TARGET)
# 链接
$(TARGET): $(OBJS) | $(BIN_DIR)
$(CC) $^ -o $@ $(LDFLAGS)
# 编译
$(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)
# 伪目标
.PHONY: all clean
这个Makefile有几个亮点:
- 自动创建目录:用
|表示“仅当目录不存在时才创建”,不会触发重新编译 - 伪目标:
.PHONY告诉make,all和clean不是文件,别去检查它们的时间戳 - 目录分离:源文件、头文件、目标文件、输出文件各归各的,整洁
避坑指南:我曾经在 clean 目标里直接写 rm -rf $(OBJ_DIR),结果有一次 OBJ_DIR 变量没定义,变成了 rm -rf……嗯,你懂的。从那以后,我都在 clean 前面加个检查:
clean:
@if [ -n "$(OBJ_DIR)" ]; then rm -rf $(OBJ_DIR); fi
@if [ -n "$(BIN_DIR)" ]; then rm -rf $(BIN_DIR); fi
4.5 条件判断与多目标
有时候,同一个工程要编译成调试版和发布版。用条件判断就能搞定:
DEBUG ?= 0
ifeq ($(DEBUG), 1)
CFLAGS += -g -DDEBUG
else
CFLAGS += -O2
endif
编译调试版:make DEBUG=1。编译发布版:直接 make。简单吧?
多目标也是常见需求。比如同时编译两个程序:
all: server client
server: server.o common.o
$(CC) $^ -o $@
client: client.o common.o
$(CC) $^ -o $@
这样 make 一次,两个程序都出来了。
4.6 总结
Makefile说白了就是一套规则+变量+函数的组合。规则告诉make“怎么做”,变量让规则更灵活,函数让变量更智能。三者结合,就能实现“加个文件不用改Makefile”的自动化编译。
我个人觉得,学Makefile最好的方法就是动手写。从最简单的单文件开始,慢慢加变量、加函数、加目录结构。写坏了就 make clean 重来。折腾几次,你就发现它其实没那么神秘。
嗯,这一章就到这儿。记住:Makefile不是写给人看的,是写给make看的。但写得好,人看着也舒服。
核心要点回顾:
- 规则三要素:目标、前提、命令(命令前必须是Tab)
- 变量用
$()引用,内置变量$@、$<、$^很实用 wildcard和patsubst是实现自动化的关键函数- 模式规则
%.o: %.c避免重复写规则 - 伪目标
.PHONY防止文件名冲突
可可精品资料库