7、Makefile 入门:Makefile 基本语法、目标与依赖、变量使用、伪目标 .PHONY

说实话,很多C语言开发者写代码一把好手,但一提到Makefile就头疼。我当年刚入行时也是这样,总觉得有IDE就够了,干嘛要学这个?直到有一次参与一个大型嵌入式项目,几十个源文件手动编译,改一个文件就要全部重新编译,等得我怀疑人生。嗯,从那以后,我老老实实把Makefile啃了下来。

今天我们就来聊聊Makefile的核心内容。别怕,其实它没那么复杂。

7.1 Makefile 是什么?

Makefile 说白了就是一个自动化编译脚本。它告诉 make 工具:哪些文件需要编译、用什么命令编译、文件之间有什么依赖关系。

你想想看,一个项目少则几十个文件,多则成百上千。如果每次改一行代码都要手动敲 gcc 命令,那效率也太低了。Makefile 就是来解决这个问题的。

核心思想: Makefile 通过比较目标文件和依赖文件的时间戳,决定哪些文件需要重新编译。只有发生变化的文件才会被重新编译,大大节省时间。

7.2 基本语法:目标与依赖

Makefile 的基本规则其实就一句话:

目标(target): 依赖(dependencies)
    命令(command)

我来解释一下:

  • 目标:通常是要生成的文件名,比如 main.o、app
  • 依赖:生成目标所需要的文件
  • 命令:如何从依赖生成目标,注意前面必须是一个 Tab 键,不能是空格

举个最简单的例子:

app: main.c utils.c
    gcc -o app main.c utils.c

这个 Makefile 的意思是:要生成 app 这个可执行文件,需要 main.c 和 utils.c 这两个源文件。如果它们中任何一个有修改,就执行 gcc 命令重新编译。

注意: 命令前面的缩进必须是 Tab,不能是空格。这是新手最容易犯的错误,我曾经因为这个排查了半小时,最后发现是编辑器把 Tab 自动转成了空格。

7.3 变量使用:让 Makefile 更灵活

硬编码文件名和编译器选项,显然不够优雅。我在项目中习惯用变量来管理这些信息,这样修改起来方便多了。

Makefile 中定义变量很简单:

CC = gcc
CFLAGS = -Wall -O2
TARGET = app
SRCS = main.c utils.c

$(TARGET): $(SRCS)
    $(CC) $(CFLAGS) -o $(TARGET) $(SRCS)

使用变量时用 $(变量名) 来引用。这样做的好处很明显:

  • 想换编译器?改 CC 变量就行
  • 想加编译选项?改 CFLAGS 变量就行
  • 想增加源文件?改 SRCS 变量就行

我个人习惯把常用的变量放在 Makefile 开头,一目了然。

7.4 伪目标 .PHONY

这个知识点,我建议你重点理解。伪目标是什么?

先看一个场景:我们经常会在 Makefile 里写一个 clean 目标,用来清理编译产物:

clean:
    rm -f *.o app

然后执行 make clean 就能清理文件。看起来没问题对吧?

但假如你的项目目录里恰好有一个叫 clean 的文件呢?

这时候 make 会认为 clean 这个目标已经是最新的了(因为 clean 文件存在且没有依赖),于是就不执行任何命令。这就是个大坑!

解决方案: 使用 .PHONY 声明伪目标,告诉 make 这个目标不代表实际文件,每次都执行命令。

.PHONY: clean

clean:
    rm -f *.o app

常见的伪目标还有:

  • all:默认目标,通常放在第一个
  • clean:清理编译产物
  • install:安装程序
  • test:运行测试

我曾经在一个项目中忘记加 .PHONY,结果同事在目录下创建了一个叫 clean 的日志文件,导致整个团队的清理脚本都失效了。排查了半天才发现是这个问题。

7.5 一个完整的示例

把上面的知识点串起来,一个典型的 Makefile 长这样:

CC = gcc
CFLAGS = -Wall -O2 -I./include
LDFLAGS = -lm
TARGET = app
SRCS = main.c utils.c sensor.c
OBJS = $(SRCS:.c=.o)

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJS)
    $(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
    $(CC) $(CFLAGS) -c -o $@ $<

clean:
    rm -f $(OBJS) $(TARGET)

这里有几个新东西我解释一下:

  • $@:表示当前目标的名字
  • $^:表示所有依赖文件
  • $<:表示第一个依赖文件
  • %.o: %.c:模式规则,意思是所有 .o 文件都由对应的 .c 文件编译生成

这些自动化变量能省不少事,我建议你记住它们。

7.6 知识体系图

下面这张图帮你梳理 Makefile 的核心知识点:

Makefile 核心 目标与依赖 命令(Tab缩进) 变量使用 CC、CFLAGS、SRCS .PHONY 伪目标 clean、all、install 自动化变量 $@、$^、$< 模式规则 %.o: %.c 掌握这些,日常项目够用了

7.7 避坑指南

最后分享几个我踩过的坑:

  • Tab 和空格:Makefile 的命令行必须用 Tab 缩进,用空格会报错。我建议你在编辑器里设置显示不可见字符,一眼就能看出来。
  • .PHONY 别忘了:只要目标不代表实际文件,就加上 .PHONY 声明。养成习惯,避免踩坑。
  • 变量引用用括号:单字符变量可以省略括号,比如 $@,但多字符变量必须加括号,比如 $(CC)。我统一都加括号,省得记混。
  • 命令执行失败:默认情况下,如果某条命令执行失败,make 会停止。可以在命令前加 - 忽略错误,比如 -rm -f *.o

小技巧: 调试 Makefile 时可以用 make -p 查看所有变量和规则的展开结果,非常有用。

好了,Makefile 入门就讲到这里。记住,写 Makefile 的核心思路就是:告诉 make 你的项目结构,让它帮你做重复的编译工作。多用变量,少写死路径,加上 .PHONY 保平安。这些习惯养成了,你的项目构建就会越来越顺手。


公众号:蓝海资料掘金营,微信deep3321