title: Makefile created: 2026-08-31 type: source tags:
# 小项目手动编译
gcc main.c utils.c -o app
# 大项目手动编译(噩梦)
gcc src/main.c src/utils.c src/network.c src/crypto.c \
-I./include -L./lib -lssl -lcrypto -lpthread -o app
# 修改一个文件后,全部重新编译 → 浪费时间
| 问题 | Makefile 的解决方案 |
|---|---|
| 编译命令繁琐 | 一条 make 搞定 |
| 全量编译浪费 | 增量编译:只重建修改过的文件及其依赖 |
| 依赖关系混乱 | 显式声明文件间依赖关系 |
| 多目标管理 | make clean、make install 等伪目标 |
| 跨平台/跨工具链 | 变量机制,换工具链只需改一个变量 |
target: prereq1 prereq2
command1
command2
.o 或可执行文件)1. 读取 Makefile
2. 找到第一个目标(默认目标)
3. 检查目标是否存在、依赖是否比目标新
4. 如果依赖更新或目标不存在 → 执行命令
5. 递归处理依赖(如果依赖也是目标)
核心机制:时间戳比较
如果 依赖文件的修改时间 > 目标文件的修改时间 → 重建目标
否则 → 跳过(认为目标已是最新)
# 一个完整的、可编译的最小示例
app: main.o utils.o
gcc main.o utils.o -o app
main.o: main.c utils.h
gcc -c main.c -o main.o
utils.o: utils.c utils.h
gcc -c utils.c -o utils.o
clean:
rm -f *.o app
# 使用
make # 编译
make clean # 清理
| 运算符 | 名称 | 行为 | 示例 |
|---|---|---|---|
= |
延迟展开 | 使用时才求值 | OBJS = $(SRC:.c=.o) |
:= |
立即展开 | 赋值时立刻求值 | OBJS := main.o utils.o |
?= |
条件赋值 | 仅当变量未定义时赋值 | CC ?= gcc |
+= |
追加 | 在现有值后追加内容 | CFLAGS += -Wall |
# 延迟展开 vs 立即展开
CC = gcc # 延迟展开(用时求值)
OBJS := main.o utils.o # 立即展开(定义时求值)
# 条件赋值:允许外部覆盖
CC ?= gcc # 默认 gcc,但 make CC=clang 可覆盖
# 追加
CFLAGS = -Wall
CFLAGS += -O2 # 现在 CFLAGS = "-Wall -O2"
CC = gcc
CFLAGS = -Wall -O2
# 引用变量
$(CC) $(CFLAGS) main.c -o main
# 等价于
gcc -Wall -O2 main.c -o main
| 变量 | 含义 | 示例 |
|---|---|---|
$@ |
当前目标 | app: main.o → gcc main.o -o $@ |
$< |
第一个依赖 | main.o: main.c → gcc -c $< -o $@ |
$^ |
所有依赖(去重) | app: a.o b.o → gcc $^ -o $@ |
$? |
比目标新的依赖 | 只传递修改过的文件 |
$* |
模式匹配的词干 | %.o: %.c 中,$* 是不带后缀的文件名 |
# 使用自动变量改写
app: main.o utils.o
gcc $^ -o $@ # gcc main.o utils.o -o app
main.o: main.c utils.h
gcc -c $< -o $@ # gcc -c main.c -o main.o
% 和 *| 符号 | 使用场景 | 含义 |
|---|---|---|
% |
规则中 | 匹配任意长度的文件名前缀(词干) |
* |
文件名中 | Shell 风格通配符,匹配任意字符 |
# * 通配符 — 在变量或依赖中匹配文件
SRCS = $(wildcard src/*.c) # 获取 src/ 下所有 .c 文件
OBJS = $(SRCS:.c=.o) # 将 .c 替换为 .o
# % 通配符 — 在规则中匹配
%.o: %.c
gcc -c $< -o $@
# 所有 .c → .o 的通用规则
%.o: %.c
gcc -c $< -o $@
# 指定依赖
%.o: %.c include/*.h
gcc -Wall -O2 -c $< -o $@
Make 内置了隐含规则:
# 以下规则可以省略(make 自动推导)
main.o: main.c
# make 自动执行 gcc -c main.c -o main.o
建议: 始终显式写出规则,避免依赖隐含行为。
| 函数 | 作用 | 示例 |
|---|---|---|
$(wildcard pat) |
匹配文件 | $(wildcard src/*.c) |
$(patsubst p,r,t) |
模式替换 | $(patsubst %.c,%.o,src/*.c) |
$(notdir names) |
取文件名(去掉路径) | $(notdir src/main.c) → main.c |
$(dir names) |
取目录 | $(dir src/main.c) → src/ |
$(foreach v,l,t) |
循环 | $(foreach d,$(DIRS),$(wildcard $(d)/*.c)) |
# 项目结构:
# project/
# ├── include/ # 头文件
# │ ├── utils.h
# │ └── network.h
# ├── src/ # 源文件
# │ ├── main.c
# │ ├── utils.c
# │ └── network/
# │ ├── tcp.c
# │ └── udp.c
# ├── lib/ # 外部库
# └── Makefile
CC = gcc
CFLAGS = -Wall -O2 -I./include
# 递归查找所有 .c 文件
SRCS = $(shell find src -name '*.c')
OBJS = $(SRCS:.c=.o)
# 自动创建输出目录
OBJDIR = build
$(shell mkdir -p $(OBJDIR))
TARGET = app
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $^ -o $@
# 模式规则:所有 .c 编译为 .o,放入 build 目录
$(OBJDIR)/%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@
clean:
rm -rf $(OBJDIR) $(TARGET)
.PHONY: all clean
# ============================================
# 嵌入式交叉编译 Makefile 模板
# ============================================
# 交叉编译工具链
CROSS_COMPILE ?= arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
AR = $(CROSS_COMPILE)ar
STRIP = $(CROSS_COMPILE)strip
# 编译选项
CFLAGS = -Wall -O2 -g
CFLAGS += -I./include
CFLAGS += -DDEBUG -DVERSION=\"1.0\"
CFLAGS += -std=c11
# 链接选项
LDFLAGS = -static
LIBS = -lpthread
# 目标
TARGET = app
# 源文件与目标文件
SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
# 默认目标
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^ $(LIBS)
$(STRIP) $@ # 去掉调试符号,减小体积
@echo "Build complete: $(TARGET)"
# 通用编译规则
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 头文件依赖自动生成(-MMD -MP)
-include $(OBJS:.o=.d)
%.o: %.c
$(CC) $(CFLAGS) -MMD -MP -c -o $@ $<
# 清理
clean:
rm -f $(OBJS) $(OBJS:.o=.d) $(TARGET)
# 安装(通常用于根文件系统部署)
DESTDIR ?= /usr/local
install: $(TARGET)
install -d $(DESTDIR)/bin
install -m 755 $(TARGET) $(DESTDIR)/bin/
.PHONY: all clean install
# -MMD 生成 .d 文件(依赖关系),如 main.d:
# main.o: main.c utils.h config.h
# -MP 生成空规则防止头文件删除时报错
-include $(OBJS:.o=.d)
# 每次编译时自动生成依赖,下次 make 自动检查头文件变化
.PHONY: all clean install test
all: $(TARGET)
clean:
rm -f $(OBJS) $(TARGET)
install:
cp $(TARGET) /usr/local/bin/
test:
./run_tests.sh
为什么要声明
.PHONY? 如果当前目录下有名为clean的文件,make clean会因文件已存在而跳过。声明伪目标可避免此问题。
# 根据平台选择不同选项
ifeq ($(OS),Windows_NT)
TARGET = app.exe
RM = del /Q
else
TARGET = app
RM = rm -f
endif
# 根据编译类型选择
ifdef DEBUG
CFLAGS += -g -O0
else
CFLAGS += -O2
endif
# 使用
make DEBUG=1 # 调试版本
make # 发布版本
SRCS = src/main.c src/utils.c src/net/tcp.c
# 取所有文件名(去掉路径)
$(notdir $(SRCS)) # → main.c utils.c tcp.c
# 模式替换
$(patsubst %.c,%.o,$(SRCS)) # → src/main.o src/utils.o src/net/tcp.o
# 简写
$(SRCS:.c=.o) # → 同上
# 循环
DIRS = src include lib
$(foreach d,$(DIRS),$(wildcard $(d)/*.c))
OBJS = main.o utils.o network.o
# 只对这些特定文件应用规则
$(OBJS): %.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
# 同一规则生成多个目标
all: libfoo.so libfoo.a
libfoo.so: foo.o
$(CC) -shared -o $@ $^
libfoo.a: foo.o
$(AR) rcs $@ $^
# 在编译前确保目录存在
OBJDIR = build
$(shell mkdir -p $(OBJDIR))
$(OBJDIR)/%.o: %.c
$(CC) -c $< -o $@
# 打印变量值,排查 Makefile 问题
all:
@echo "SRCS = $(SRCS)"
@echo "OBJS = $(OBJS)"
@echo "CC = $(CC)"
# ============================================
# Makefile — LED 控制应用(嵌入式交叉编译)
# ============================================
CROSS_COMPILE ?= arm-linux-gnueabihf-
CC = $(CROSS_COMPILE)gcc
STRIP = $(CROSS_COMPILE)strip
CFLAGS = -Wall -O2 -g -I./include
CFLAGS += -DUSE_GPIO -DDEVICE=\"/dev/gpio0\"
LDFLAGS = -static
TARGET = led_app
SRCS = src/main.c src/led.c src/gpio.c
OBJS = $(SRCS:.c=.o)
all: $(TARGET)
$(TARGET): $(OBJS)
$(CC) $(LDFLAGS) -o $@ $^
$(STRIP) $@
@echo "=== Build complete: $(TARGET) ==="
%.o: %.c
$(CC) $(CFLAGS) -c -o $@ $<
clean:
rm -f $(OBJS) $(TARGET)
# 烧录到设备(示例)
deploy: $(TARGET)
scp $(TARGET) root@192.168.1.100:/usr/bin/
ssh root@192.168.1.100 "chmod +x /usr/bin/$(TARGET)"
.PHONY: all clean deploy
@ 前缀:@echo 不打印命令本身,只打印输出- 前缀:-rm 表示忽略错误继续执行$ 要转义:在 Shell 命令中用 $$ 表示 Shell 变量,如 $$HOMEmake -j4 开启 4 个并行任务,大幅加速编译make 默认执行第一个目标:将 all 放在最前面-MMD -MP 避免手动维护头文件依赖A: 基于时间戳的增量编译。make 比较目标文件和依赖文件的修改时间,如果依赖更新则重建目标。只重建需要更新的部分,避免全量编译。
= 和 := 的区别?A: = 延迟展开(使用时求值),:= 立即展开(定义时求值)。OBJS = $(SRC:.c=.o) 用 = 时,SRC 后续修改会影响 OBJS;用 := 则不会。
$@、$<、$^ 分别是什么?A: 自动变量在规则执行时自动赋值:$@ 是当前目标,$< 是第一个依赖,$^ 是所有依赖(去重)。用自动变量可以写出通用的模式规则。
A: 用 $(wildcard src/**/*.c) 或 $(shell find src -name '*.c') 递归查找源文件,用模式规则 %.o: %.c 配合 -o $@ 输出到对应目录,用 -MMD -MP 自动生成头文件依赖。
-MMD -MP 的作用?A: -MMD 在编译时自动生成 .d 依赖文件(记录目标依赖哪些头文件),-MP 生成空规则防止头文件被删除后 make 报错。通过 -include $(OBJS:.o=.d) 引入,实现头文件变更时自动重新编译。
A: 这是 make 的语法约定。make 用 Tab 来区分规则定义和命令。如果用空格缩进,make 会将其误认为规则的一部分而报语法错误。
A: 1)设置正确的 CROSS_COMPILE 前缀;2)使用 -static 避免目标设备缺动态库;3)交叉编译的工具链路径需正确;4)可能需要指定 --sysroot 指向目标系统的根文件系统。
A: 1)make -n(dry-run)打印要执行的命令但不执行;2)make -p 打印完整规则数据库;3)在规则中加 @echo 打印变量值;4)make --debug=b 显示决策原因。