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 $@

这段代码干了什么?

  1. wildcard 自动找到 src/ 下所有 .c 文件
  2. patsubst 把 src/xxx.c 变成 obj/xxx.o
  3. 模式规则 $(OBJ_DIR)/%.o: $(SRC_DIR)/%.c 自动匹配每个文件

现在你加一个新 .c 文件到 src/ 目录,Makefile 不用改一行。这就是自动化编译的威力。

我曾经踩过的坑:wildcard 匹配不到文件时,不会报错,而是返回空字符串。然后 OBJS 就空了,链接时啥也没有。所以建议加个检查:

ifeq ($(strip $(SRCS)),)
$(error 没有找到源文件!)
endif

4.4 自动化编译:从手动到自动

好了,前面都是基础。现在咱们把前面学的串起来,搞一个真正能用的自动化编译系统。

先画个图,看看整体流程:

Makefile 自动化编译流程 *.c 源文件 *.h 头文件 依赖检查 编译 (gcc -c) 生成 .o 文件 链接 (gcc -o) 生成可执行文件 头文件变更触发重编译 make clean 删除 .o 和可执行文件 重新编译

流程其实不复杂:源文件和头文件经过编译生成目标文件(.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 防止文件名冲突

可可精品资料库