2、Makefile核心语法(一):规则三要素、第一个Makefile、变量基础与自动变量

好,咱们正式开始动手写 Makefile 了。这一章是地基,打不牢后面全得塌。我见过不少同事,上来就抄网上的通用 Makefile,结果一改交叉编译链就崩,根本不知道哪里出了问题。说白了,就是核心语法没吃透。

这一章我会把 Makefile 最核心的四个知识点揉碎了讲:规则三要素、第一个 Hello World、四种变量赋值,还有三个最常用的自动变量。你跟着我一步步来,保证能写出第一个能用的 Makefile。

Makefile 核心语法(一)知识体系 Makefile 核心语法 规则三要素 第一个 Makefile 变量基础 自动变量 目标 依赖 命令 = 延时展开 := 立即展开 ?= 条件赋值 += 追加赋值 $@ 目标 $^ 所有依赖 $< 第一个依赖 💡 学习路径建议 先掌握规则三要素 → 写第一个 Makefile → 理解变量赋值差异 → 用自动变量简化规则

2.1 规则三要素:目标、依赖、命令

Makefile 的核心就一句话:一条规则 = 目标 + 依赖 + 命令。你想想看,这其实跟做饭一个道理——目标是「做出一盘番茄炒蛋」,依赖是「番茄、鸡蛋、油盐」,命令就是「切菜、打蛋、下锅炒」。Make 工具就是那个帮你盯着厨房的机器人,它只会在「依赖比目标新」的时候才动手。

语法格式长这样:

目标: 依赖1 依赖2 ...
    命令1
    命令2

注意!命令前面必须是一个 Tab 键,不能用空格代替。我当年刚入行时,就因为编辑器把 Tab 自动转成了空格,折腾了整整一个下午才找到原因。嗯,这里一定要留心。

规则执行逻辑:

  • 如果目标文件不存在 → 执行命令生成它
  • 如果依赖文件比目标文件更新 → 执行命令重新生成
  • 如果目标比所有依赖都新 → 什么都不做(跳过)

2.2 第一个 Makefile:Hello World

光说不练假把式。咱们先写一个最简单的 C 程序,然后用 Makefile 来编译它。

先创建一个 hello.c:

#include <stdio.h>

int main() {
    printf("Hello, Makefile World!\n");
    return 0;
}

然后在同目录下创建 Makefile(注意首字母大写,小写也行,但大写在 tab 补全时更显眼):

hello: hello.c
    gcc -o hello hello.c

在终端执行 make,你会看到:

$ make
gcc -o hello hello.c

再执行一次:

$ make
make: 'hello' is up to date.

看到了吗?第一次编译,第二次直接跳过。这就是 Make 的增量编译机制——只编译需要重新编译的部分。在大型项目中,这能省下大量时间。

我的习惯:每个 Makefile 我都会先写一个 clean 伪目标,方便清理中间文件:

clean:
    rm -f hello

用 make clean 就能一键清理。注意 clean 不是文件,所以最好加上 .PHONY: clean 声明它是伪目标。

2.3 变量基础:=、:=、?=、+=

写几个规则还好,一旦项目有几十个源文件,重复写文件名会让人崩溃。这时候就需要变量了。

Makefile 的变量赋值有四种方式,很多人搞混。我直接给你一张对比表:

运算符 名称 展开时机 典型场景
= 递归展开赋值 使用时才展开(延时) 定义依赖其他变量的变量
:= 简单展开赋值 定义时立即展开 固定路径、编译选项
?= 条件赋值 仅当变量未定义时赋值 提供默认值
+= 追加赋值 在原有值后追加 逐步添加源文件列表

咱们用代码说话。看这个例子:

# = 延时展开
A = hello
B = $(A) world
A = hi
# 此时 B 的值是 "hi world"(因为使用时才展开)

# := 立即展开
C := hello
D := $(C) world
C := hi
# 此时 D 的值是 "hello world"(定义时就固定了)

# ?= 条件赋值
E ?= default_value
# 如果 E 没定义过,就赋值为 default_value

# += 追加赋值
F = a
F += b
F += c
# F 最终为 "a b c"

我曾经踩过的坑:用 = 定义变量时,如果形成循环引用,Make 会报错。比如:

A = $(B)
B = $(A)

这会导致无限递归。解决办法:能用 := 的地方尽量用 :=,它更可控。

我个人习惯是:编译选项、路径等固定内容用 :=;需要根据条件变化的用 =;提供默认值用 ?=;逐步累积列表用 +=。

2.4 自动变量:$@、$^、$<

有了变量,写规则还是有点啰嗦。比如:

hello: hello.c
    gcc -o hello hello.c

目标名和依赖名各写了两次。能不能自动获取?能!自动变量就是干这个的。

三个最常用的:

自动变量 含义 示例值(规则 hello: hello.c)
$@ 目标文件名 hello
$^ 所有依赖文件列表(去重) hello.c
$< 第一个依赖文件 hello.c

用自动变量改写上面的规则:

hello: hello.c
    gcc -o $@ $^

是不是清爽多了?再也不用担心改目标名时忘了改命令里的文件名。

来个多文件的例子:

main: main.o utils.o
    gcc -o $@ $^

main.o: main.c
    gcc -c -o $@ $<

utils.o: utils.c
    gcc -c -o $@ $<

注意:

  • $^ 会自动去重,如果同一个依赖出现两次,只保留一个
  • $< 只取第一个依赖,适合单源文件编译的场景
  • 自动变量只能在规则的命令中使用,不能在依赖列表中使用