2、Makefile核心语法:目标、依赖、命令的写法,伪目标.PHONY的使用
好,咱们直接进入正题。Makefile 这东西,说白了就是一个“菜谱”。你告诉它你想做什么菜(目标),它看看材料够不够(依赖),然后按步骤开火(命令)。
我刚开始接触 Makefile 时,觉得它不就是个批处理脚本嘛。后来在项目里被坑过几次,才明白它的设计其实很精巧。今天咱们就把最核心的三个要素讲透。
2.1 目标(Target)——你要造什么?
目标就是 Makefile 里冒号左边的东西。它通常是一个文件名,比如 main.o、app。但也可以是“伪目标”,这个后面细说。
app: main.o utils.o
gcc -o app main.o utils.o
这里的 app 就是目标。Make 看到它,会先检查依赖文件有没有更新,再决定要不要执行下面的命令。
2.2 依赖(Prerequisites)——你的材料备齐了吗?
依赖写在目标后面,用空格隔开。它们可以是源文件、目标文件,甚至是另一个目标。
main.o: main.c header.h
gcc -c main.c
这里 main.o 依赖 main.c 和 header.h。只要这两个文件有一个比 main.o 新,Make 就会重新编译。
为什么会这样?因为 Make 比较的是文件的时间戳。嗯,这里要注意:如果你用 git 切换分支,时间戳可能会乱掉,导致不必要的重编译。我曾经因为这个浪费了半天时间,后来加了 touch 命令才搞定。
2.3 命令(Recipe)——动手开干!
命令就是目标下面缩进的行,必须以 Tab 开头。这是新手最容易踩的坑——用空格代替 Tab,Make 直接报错。
clean:
rm -f *.o app
命令可以有多行,每行都在独立的 shell 中执行。如果你想在同一 shell 里执行多条命令,可以用分号或反斜杠连接。
cat -A Makefile,Tab 会显示为 ^I。
2.4 伪目标 .PHONY——别把“任务”当文件
你想想看,如果当前目录下恰好有一个叫 clean 的文件,那 make clean 会怎样?Make 发现 clean 文件已经存在,而且没有依赖,就认为“目标已是最新”,什么都不做。
这就是伪目标存在的意义。用 .PHONY 告诉 Make:这个目标不是真实文件,别拿文件时间戳来比较,直接执行命令。
.PHONY: clean all install
clean:
rm -f *.o app
all: app
install:
cp app /usr/local/bin/
我在项目中遇到过好几次这种问题:明明改了代码,执行 make clean 却没有任何反应。查了半天,发现目录下有个叫 clean 的临时文件。从那以后,我所有“动作型”目标都会加上 .PHONY。
- 目标如果是文件名,Make 会检查时间戳
- 目标如果是动作(clean、all、install),必须声明为伪目标
- .PHONY 可以写在目标前面,也可以集中写在文件开头
2.5 一个完整的例子
咱们把上面这些串起来,写个完整的 Makefile:
CC = gcc
CFLAGS = -Wall -O2
.PHONY: all clean
all: app
app: main.o utils.o
$(CC) -o $@ $^
main.o: main.c header.h
$(CC) $(CFLAGS) -c main.c
utils.o: utils.c utils.h
$(CC) $(CFLAGS) -c utils.c
clean:
rm -f *.o app
这里 $@ 表示目标名,$^ 表示所有依赖。这些是自动变量,后面章节会详细讲。你只要知道它们能帮你少写很多重复代码就行。
2.6 知识体系图
下面这张图帮你理清 Makefile 核心语法的关系:
2.7 避坑指南
- Tab 问题:我见过最离谱的,是有人用记事本编辑 Makefile,Tab 被自动转成 4 个空格。解决方案:用
vim或VS Code设置editor.insertSpaces: false。 - 伪目标遗漏:如果你有一个
test目标,而目录下恰好有test文件夹,Make 会认为目标已是最新。我曾经在 CI 环境里遇到过,排查了半小时才发现是这个问题。 - 命令执行顺序:Make 默认并行执行不相关的目标。如果你有依赖关系,一定要在依赖里写清楚,别指望命令顺序。
make -n 可以预览要执行的命令,不会真的运行。调试时特别好用。
好了,核心语法就这些。说白了就是三要素:目标、依赖、命令。再加上 .PHONY 这个护身符,基本能应付大部分场景。下一节咱们聊聊变量和自动变量,那才是 Makefile 真正灵活的地方。
可可精品资料库