2. 构建系统概述
好,咱们正式开始动手写 Makefile 了。这一章是地基,打不牢后面全得塌。我见过不少同事,上来就抄网上的通用 Makefile,结果一改交叉编译链就崩,根本不知道哪里出了问题。说白了,就是核心语法没吃透。
这一章我会把 Makefile 最核心的四个知识点揉碎了讲:规则三要素、第一个 Hello World、四种变量赋值,还有三个最常用的自动变量。你跟着我一步步来,保证能写出第一个能用的 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 $@ $<
注意:
$^会自动去重,如果同一个依赖出现两次,只保留一个$<只取第一个依赖,适合单源文件编译的场景- 自动变量只能在规则的命令中使用,不能在依赖列表中使用