【Linux 项目自动化构建工具:make/makefile 一篇讲透】

文章目录
前言
你是否有过这样的经历?
- 写了一个多文件的 C 项目,每次编译都要敲一长串
gcc命令 - 改了其中一个文件,却要重新编译所有文件,浪费大量时间
- 听说有个
make命令,但不知道 makefile 怎么写
别急,这篇文章就是为你准备的。我们把 make/makefile 的来龙去脉、核心概念、实际用法,一次性讲清楚。
🍔 比喻:make/makefile 就像一个智能厨师。你告诉它“我想吃番茄炒蛋”(目标),它知道需要番茄和鸡蛋(依赖),然后按照菜谱(依赖方法)给你做出来。如果你已经做好了,它就不会重复做。
一、make 和 makefile 是什么?
| 概念 | 本质 | 比喻 |
|---|---|---|
| make | 一个命令 | 厨师本人 |
| makefile | 一个文件(存放编译规则) | 菜谱 |
关系:make 命令会读取 makefile 文件,按照里面的规则来编译程序。
# 基本用法
make # 根据 makefile 编译项目
make clean # 执行清理操作(如果定义了 clean)
二、makefile 的核心概念
2.1 依赖关系和依赖方法
| 概念 | 含义 | 示例 |
|---|---|---|
| 依赖关系 | 目标文件由哪些源文件生成 | code 依赖 code.c |
| 依赖方法 | 如何从依赖生成目标 | gcc -o code code.c |
2.2 一个最简单的 makefile 示例
# 依赖关系:code 这个目标依赖于 code.c
code: code.c
# 依赖方法:用 gcc 编译(注意:前面必须是一个 Tab 键,不能是空格!)
gcc -o code code.c
执行:
make
# 会执行:gcc -o code code.c
⚠️ 巨坑提醒:依赖方法前面的缩进必须是 Tab 键,不能是空格!这是新手 99% 会踩的坑。如果你用的是 VS Code,记得把编辑器设置为「Tab 转空格」关闭。
三、makefile 的工作流程
3.1 默认执行第一个目标
make 命令扫描 makefile 时,从上到下,默认执行第一个目标文件。
# 第一个目标是 code,所以 make 默认执行这个
code: code.c
gcc -o code code.c
clean:
rm -f code
make # 只执行 code 的编译,不会执行 clean
3.2 如何判断是否需要重新编译?
这是 make 最智能的地方:通过比较时间戳。
核心逻辑:
· 如果目标文件不存在 → 执行编译
· 如果目标文件存在 → 比较目标和依赖的修改时间
· 依赖文件(.c)比目标文件(code)更新 → 重新编译
· 依赖文件比目标文件更旧 → 不编译(提示:make: ‘code’ is up to date.)
🕐 比喻:就像你检查牛奶是否过期。生产日期(依赖时间)比当前时间(目标时间)新?说明需要重新买。
四、文件的三个时间(ACM 时间)
4.1 什么是 ACM 时间?
在 Linux 中,每个文件都有三个时间戳:
| 时间 | 全称 | 含义 | 什么时候改变 |
|---|---|---|---|
| Access | atime | 最后访问时间 | cat、less、vim 打开文件 |
| Modify | mtime | 最后内容修改时间 | echo >、vim 修改内容 |
| Change | ctime | 最后属性修改时间 | chmod、chown、修改内容也会改 |
4.2 三者之间的关系
# 查看文件的时间
stat code.c
输出示例:
File: code.c
Size: 1024 Blocks: 8 IO Block: 4096 regular file
Access: 2024-05-17 10:00:00.000000000 +0800
Modify: 2024-05-17 09:00:00.000000000 +0800
Change: 2024-05-17 09:00:00.000000000 +0800
重要结论:
· 修改文件内容时,Modify 和 Change 都会变
· 只修改文件权限(chmod)时,只有 Change 变
4.3 make 用哪个时间来判断?
make 通过 Modify 时间来判断文件新旧。
# 手动更新文件的 Modify 时间
touch code.c
# 此时 make 会认为 code.c 比 code 新,触发重新编译
make
💡 touch 命令可以更新文件的三个时间到当前时间。
五、清理操作:clean 和 .PHONY
5.1 为什么要 clean?
| 清理对象 | 为什么要删 |
|---|---|
code(可执行文件) |
重新编译时从头生成,避免旧程序残留 |
*.o(目标文件) |
保证完全重新编译,避免链接错误 |
clean:
rm -f code *.o
· -f:强制删除,文件不存在也不报错
· *.o:通配符,删除所有以 .o 结尾的文件
5.3 .PHONY:伪目标
.PHONY: clean
clean:
rm -f code *.o
.PHONY 的作用:告诉 make,clean 不是一个真实的文件,而是一个伪目标,总是执行它的依赖方法。
什么情况下会“不执行”?
没有 .PHONY 声明时,如果当前目录下恰好有一个名为 clean 的文件,make 会比较 clean 文件和它的依赖(如果有),判断是否需要执行。这会导致 clean 命令不执行,非常坑!
# 假设当前目录有一个叫 clean 的文件
touch clean
# 没有 .PHONY 的话,执行 make clean 会提示
make clean
# make: 'clean' is up to date. (不会执行删除!)
# 加了 .PHONY 后,无论有没有 clean 文件,都会执行
🎯 最佳实践:所有不生成同名文件的目标,都应该用 .PHONY 声明。
六、makefile 中的变量
6.1 为什么要用变量?
· 避免重复书写
· 方便修改(改一处即可)
6.2 变量定义和使用
# 定义变量
BIN = code
CC = gcc
SRC = code.c
OBJ = code.o
FLAGS = -o
# 使用变量:$(变量名)
$(BIN): $(OBJ)
$(CC) $(FLAGS) $@ $^
6.3 自动变量(重要!)
| 自动变量 | 含义 | 示例值 |
|---|---|---|
$@ |
目标文件 | code |
$^ |
所有依赖文件 | code.o(如果有多个依赖,就是 a.o b.o c.o) |
$< |
第一个依赖文件 | code.o |
BIN = code
CC = gcc
SRC = code.c
OBJ = code.o
# 生成 .o 文件
$(OBJ): $(SRC)
$(CC) -c $< -o $@
# 生成可执行文件
$(BIN): $(OBJ)
$(CC) $^ -o $@
# 清理
.PHONY: clean
clean:
rm -f $(OBJ) $(BIN)
七、多文件项目实战
7.1 项目结构
project/
├── main.c
├── add.c
├── add.h
├── sub.c
├── sub.h
└── makefile
7.2 源文件内容
// add.h
#ifndef ADD_H
#define ADD_H
int add(int a, int b);
#endif
// add.c
#include "add.h"
int add(int a, int b) {
return a + b;
}
// sub.h
#ifndef SUB_H
#define SUB_H
int sub(int a, int b);
#endif
// sub.c
#include "sub.h"
int sub(int a, int b) {
return a - b;
}
// main.c
#include <stdio.h>
#include "add.h"
#include "sub.h"
int main() {
printf("10 + 20 = %d\n", add(10, 20));
printf("10 - 20 = %d\n", sub(10, 20));
return 0;
}
7.3 makefile 写法
# 变量定义
BIN = app
CC = gcc
CFLAGS= -Wall -g
OBJS = main.o add.o sub.o
# 默认目标
$(BIN): $(OBJS)
$(CC) $^ -o $@
# 编译每个 .c 为 .o
main.o: main.c add.h sub.h
$(CC) -c $< -o $@
add.o: add.c add.h
$(CC) -c $< -o $@
sub.o: sub.c sub.h
$(CC) -c $< -o $@
# 清理
.PHONY: clean
clean:
rm -f $(OBJS) $(BIN)
# 重新编译(清理 + 编译)
.PHONY: rebuild
rebuild: clean $(BIN)
7.4 简化版(使用通配符)
BIN = app
CC = gcc
CFLAGS= -Wall -g
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
$(BIN): $(OBJS)
$(CC) $^ -o $@
%.o: %.c
$(CC) -c $< -o $@
.PHONY: clean
clean:
rm -f $(OBJS) $(BIN)
解释:
· $(wildcard *.c):获取所有 .c 文件
· $(SRCS:.c=.o):把所有 .c 替换成 .o
八、makefile 常用函数和技巧
8.1 常用函数
| 函数 | 语法 | 作用 |
|---|---|---|
wildcard |
$(wildcard pattern) |
获取匹配模式的文件列表 |
patsubst |
$(patsubst pattern,replacement,text) |
模式替换 |
shell |
$(shell command) |
执行 shell 命令 |
8.2 实用技巧
# 打印变量调试
print:
@echo "BIN = $(BIN)"
@echo "OBJS = $(OBJS)"
# 使用 shell 命令获取信息
CURRENT_DIR = $(shell pwd)
# 条件判断
ifeq ($(CC), gcc)
CFLAGS += -D_GNU_SOURCE
endif
九、常见问题速查
| 问题 | 原因 | 解决方法 |
|---|---|---|
make: Nothing to be done for 'code' |
目标已是最新 | 修改源文件或用 touch 更新 |
Makefile:2: *** missing separator. Stop. |
缩进用了空格 | 改成 Tab 键 |
make: 'clean' is up to date. |
存在 clean 文件且无 .PHONY |
添加 .PHONY: clean |
undefined reference to 'func' |
链接时缺少目标文件 | 检查 OBJS 是否包含所有 .o |
总结速查表
| 知识点 | 核心内容 |
|---|---|
make |
命令,读取 makefile 执行编译 |
makefile |
文件,定义编译规则 |
| 依赖关系 | 目标 ← 依赖 |
| 依赖方法 | 如何从依赖生成目标(Tab 缩进) |
.PHONY |
声明伪目标,总是执行 |
| ACM 时间 | Access、Modify、Change |
| make 判断新旧 | 比较 Modify 时间 |
| 自动变量 | $@(目标)、$^(所有依赖)、$<(第一个依赖) |
| 变量使用 | $(变量名) |
| 通配符 | *.o、$(wildcard *.c) |
最后
make/makefile 是 Linux 开发的必备技能,核心就三点:
- 依赖关系:目标由什么生成
- 依赖方法:怎么生成(Tab 缩进)
- 时间判断:依赖比目标新就重新编译
掌握这些,你就可以轻松管理任何规模的项目了。
更多推荐




所有评论(0)