简介:GNU make官方手册的中文翻译速查版,面向Linux驱动开发者与需要编写makefile的工程人员,用于理解依赖关系、自动增量编译与构建规则。压缩包内含1个docx文档,整体仅66KB,轻量便携,可直接检索阅读。该文档已吸引182人学习下载,覆盖make概述、makefile基本结构与规则、如何阅读手册、问题与Bug提交、特殊目标等章节,并列出GNU make增强特性及与其他make工具的不兼容点,便于进阶对比。借助第2章的Makefile简介和第4.8节特殊目标等内容,读者可从零搭建自己的编译规则;配合第9.7节选项总览与附录A快速索引,日常开发时可快速定位所需用法。对希望系统掌握make机制、减少重复编译操作、保证构建流程一致性的Linux使用者而言,这份速成手册是一份高性价比的参考。
1. makefile 速成手册:先跑通,再谈优雅
你大概率是被编译逼来的:手动敲 gcc 命令敲到第十个文件,或者改了头文件之后,整个工程默默重新编译了五分钟。makefile 解决的就是这一整类问题——用脚本描述「什么文件依赖什么、怎么生成」,让 make 替你决定哪些命令该跑、哪些该跳过。
这份速成手册不打算把 GNU make 手册翻译一遍,也不是语法罗列。我按带项目时的顺序讲:先理解规则本质,再上手变量和模式规则,搭出多目录工程,然后把最常见的翻车现场连解决命令一起给你。参考过《跟我一起写 makefile》这类长文的朋友会知道,它适合通读;这份手册的定位是当字典查、当模板抄,让你 30 分钟内写出第一份自己的 makefile。
2. 吃透规则本质:目标、依赖与命令
2.1 一条规则就是一个增量化判断
makefile 的最小单位是一条规则,三要素:目标、依赖、命令。看这个最小例子:
hello: hello.c gcc -o hello hello.c执行make hello时,make 会做一次判断:如果 hello 文件不存在,或者 hello.c 比 hello 新,就执行下面那行 gcc;否则什么都不做,打印一句make: 'hello' is up to date.
这段逻辑值得细看:目标 hello 是一个真实文件,依赖 hello.c 也是。make 比较的是两者时间戳,而不是文件内容。首次编译时 hello 不存在,条件成立,执行;第二次再跑,hello.c 没动过,条件不成立,跳过。这就是增量编译的大脑。
注意命令前必须是 tab,不能是空格。这是新手遇到的第一个坑,后面避坑章会专门讲。规则可以有很多条,多个规则之间靠依赖串起来:
all: hello hello: hello.o gcc -o hello hello.o hello.o: hello.c gcc -c -o hello.o hello.cmake all 的过程是:先看 all 依赖 hello,hello 又依赖 hello.o,make 从最下面开始逐层检查,哪条目标过期就执行哪条命令。这个过程是递归下降的,规则顺序不影响依赖判断结果——make 会先解析出完整的依赖图再动手。唯一特例是默认目标:命令行不指定目标时,执行 makefile 里第一条规则的目标,所以习惯上把 all 写在最前面。
2.2 .PHONY:把“动作”从“文件名”里摘出来
如果只有文件规则,那 clean、install 这类动作怎么办?它们不是文件,而是你想执行的“命令别名”。问题恰恰出在这里:如果当前目录碰巧有一个叫 clean 的文件,make clean会认为目标已存在且没有依赖更新,直接提示 up to date,一条命令都不会执行。
.PHONY: clean clean: rm -f *.o hello.PHONY 的意思很直白:声明 clean 是伪目标,不检查文件时间戳,每次执行都无条件运行规则里的命令。凡是你不想让它跟真实文件重名的目标,比如 clean、install、test、format,都放进 .PHONY 声明里。这属于 makefile 的“后悔药”:不写可能永远不触发,一旦目录里出现同名文件就必炸;写了永远安生。
2.3 makefile 和 cmake 的区别:先分清脚本与生成器
网上一直有人在问 makefile 和 cmake 的区别。一句话版本:makefile 是构建脚本,cmake 是构建脚本生成器。CMakeLists.txt 经过 cmake 处理生成 Makefile(或 ninja 文件),之后真正干活的是 make。所以大型工程常用 cmake 来管理跨平台和配置项,但最终交付给 make 执行的还是生成出来的 makefile。
嵌入式场景里更常见的是反过来:vitis、Vivado 甚至很多 IDE 会自动为你生成一套 makefile,你点构建按钮,背后跑的就是 make。这时候你的任务不是从零写,而是会读——报错时能根据 makefile 里的第几行、哪个目标,反查出真正失败的是哪条编译命令。cmake 生成的 makefile 里你会看到 CC、CFLAGS、RM 这些变量,改编译参数的惯用入口是 cmake 的 CMAKE_C_FLAGS,而不是直接改生成文件,因为重新生成会覆盖掉。
那什么时候该手写 makefile?我的判断标准:目录里源文件数一个手数得过来、没有复杂的第三方库依赖,手写;源文件上百、依赖多、还要跨平台,用 cmake 生成 makefile,但保留手写 makefile 的理解能力,排错时用得上。两者不是互斥,是上下游关系。
3. 变量与自动变量:让 makefile 从「能跑」到「能缩放」
3.1 赋值的四种写法:=、:=、?=、+= 别混用
makefile 里写变量赋值,四个符号各有脾气。用错了不会立刻报错,但会在某个隐蔽时刻让你的变量变成一串奇怪的值。
| 写法 | 含义 | 展开时机 |
|---|---|---|
= | 递归展开 | 变量被使用时才展开,值取决于最后一次赋值 |
:= | 立即展开 | 赋值时立即展开当前值,不随后续变更 |
?= | 条件赋值 | 仅当变量未定义时才赋值 |
+= | 追加 | 在已有值后追加,以空格分隔 |
最容易踩坑的是=和:=。看这个例子:
A = $(B) B = c X := $(B) B = dA 最终等于 d,因为=不展开,A 一直记着“去取 B 当前的值”,等用到 A 时 B 已经是 d;X 等于 c,因为:=在赋值那一刻就把 B 展开成了 c,后续 B 改成 d 与 X 无关。我的习惯:凡是变量值里带着函数调用或别的变量,一律用:=,把展开时机锁死;只剩纯字符串拼接时才用=。
?=的实用场景是为用户留覆盖口子:CFLAGS ?= -O2表示你没有在命令行传 CFLAGS 时才用默认值。命令行里执行make CFLAGS=-O0能覆盖 makefile 里的所有赋值,这是排查问题时最常用的外部干预手段。
3.2 自动变量:$@、$<、$^ 是增量编译的支柱
自动变量是 make 在每条规则里替你算好的快捷引用,专门解决“我不想在命令里再写一遍目标名和依赖名”的问题。
objects = main.o utils.o app: $(objects) gcc -o $@ $^ %.o: %.c gcc -c -o $@ $<$@ 是当前规则的目标名,$< 是第一个依赖,$^ 是所有依赖去重后的完整列表。上面这段里,链接那行展开后是gcc -o app main.o utils.o;模式规则的编译行展开后是gcc -c -o main.o main.c,每个 .c 文件都会复用。
这里的关键是%.o: %.c这条模式规则。% 相当于通配符,匹配任意不含 / 的字符串,目标里的 % 和依赖里的 % 必须匹配同一段。它有个边界要注意:% 不能匹配路径分隔符,所以 src/main.c 这类带路径的文件不能用它直接处理,第 4 章的多目录方案就是为这个准备的。
还有个冷门但好用的自动变量$?,表示所有比目标新的依赖。用它写增量打包很方便:tar -cf backup.tar $?只打包本次新增的文件,比每次全量打包省时间。
3.3 wildcard 与 patsubst:文件列表不用手写
手写objects = main.o utils.o在文件少的时候没问题,但每加一个文件都要改一行,这个玩法撑不到 50 个文件。让 make 自己数文件:
sources := $(wildcard src/*.c) objects := $(patsubst %.c,%.o,$(sources))$(wildcard src/*.c)展开成 src 目录下所有 .c 文件的完整列表;$(patsubst %.c,%.o,...)把列表里的 .c 后缀换成 .o,得到对应的目标列表。整个链路的语义是“src 下有什么,我就编译什么”。新加一个 .c 文件后直接 make,不需要改 makefile——这是 makefile 最让人舒服的体验。
两个点要注意。一是赋值用:=而不是=:wildcard 是函数调用,用=会让它在每次被展开时重新扫描目录,虽然 make 有缓存,但语义上完全没必要。二是这会引入一个隐患:如果某个 .c 文件被删除,旧的 .o 文件还在磁盘上,链接时会把旧的对象文件带上。解决办法是 make clean 后重编,或者用第 5 章讲的依赖文件方案做更干净的清理。这是我在项目里踩过不止一次的真实场景。
$(patsubst %.c,%.o,$(sources))还有一个更简短的写法$(sources:.c=.o),只做固定后缀替换,效率更高,语义等价。看到两种写法都不用奇怪,它们表达的是同一件事。
4. 多目录工程:一个顶层 Makefile 怎么把 src、include、build 管起来
4.1 目录规划与变量:把路径变成可改的配置
先定目录规则,后面所有逻辑都靠这套结构展开:
project/ src/ # 源文件 include/ # 头文件 build/ # 生成的 .o 和 .d 文件 Makefile一个常见做法是只维护一个顶层 Makefile,把路径做成可配置的变量。这样做的理由是:嵌套 make(子目录各放一个 makefile,上层递归调用)会让变量的传递、路径的换算、目标的命名全部复杂化,对大多数中小项目是过度设计。
CC := gcc CFLAGS := -Wall -O2 -Iinclude LDFLAGS := LDLIBS := -lm sources := $(wildcard src/*.c) objects := $(patsubst src/%.c,build/%.o,$(sources))逻辑说明:CC 指定编译器,CFLAGS 放编译参数,-Iinclude让预处理器能在 include 目录找头文件;LDLIBS 放链接库,-lm是数学库。sources 收集 src 下所有 .c,objects 用 patsubst 把路径从src/xxx.c改成build/xxx.o,这里同时达到两个目的:把源文件和中间文件分离,以及让目标路径跟随源文件的目录结构。
参数说明:-Wall开全部常用警告,-O2是常规优化档。嵌入式和 vitis 的交叉编译场景,把 CC 改成工具链前缀即可,比如CC := arm-none-eabi-gcc,其余逻辑完全不变。这是把编译器选择从硬编码改成变量的最大收益。
4.2 模式规则:让 build/ 和 src/ 一一对应
光有对象列表还不够,得告诉 make 怎么从 src/foo.c 生成 build/foo.o:
build/%.o: src/%.c @mkdir -p build $(CC) $(CFLAGS) -c -o $@ $<模式规则里 % 同时出现在build/%.o和src/%.c,make 会把同一个匹配串代入两边,所以 build/main.o 对应 src/main.c。命令里的 @ 是关闭回显:默认情况下 make 会把要执行的命令打印出来,@ 前缀让它安静执行——mkdir 这种每个目标都要跑一次的辅助命令不打 @ 的话,日志会非常吵。
这里有个隐含问题:build 目录首次不存在时,make 不会自动创建它。所以我在命令里加了@mkdir -p build。-p 参数允许目录已存在时不报错,于是这条命令幂等,每次编译前都能保证目录在位。
命令的失败处理也值得说:make 执行规则时若某条命令返回非零,默认立即中止整个构建。想忽略某条命令的错误,在命令前加 -,比如-rm -f $(objects)。clean 目标里常见的rm -rf build如果目录不存在会返回非零,加 - 前缀能避免 make 误报失败。
4.3 头文件依赖:用 -MMD 让增量编译不翻车
现在构建能跑,但有一个血泪级隐患:默认情况下 make 不知道 .o 依赖哪些 .h。你看规则build/%.o: src/%.c,依赖里只有 .c,没有 .h。于是改动某个头文件后,引用它的 .c 文件不会被判定为过期,链接时仍在用旧对象文件——这就是“改了头文件,程序行为没变”的经典玄学现场。
解决办法是让 gcc 帮你生成依赖文件,再把它引入 makefile:
CFLAGS += -MMD -MP DEP := $(objects:.o=.d) -include $(DEP)gcc 编译时加-MMD,会在输出 .o 的同时生成同名 .d 文件,内容是build/main.o: src/main.c include/api.h这样的规则;-MP为每个头文件补一个空伪目标,避免头文件被改名或删除后 make 因找不到依赖而报错。$(objects:.o=.d)是上一章说的简写形式,把所有 .o 后缀换成 .d。-include开头的 - 表示 .d 文件不存在(首次构建还没生成)时不报错,静默跳过。
这段逻辑让 make 的依赖图从“一个源文件一行规则”升级成“自动追踪每个头文件”。之后改动 include/api.h,只有真正包含它的 .o 会被重建,其余全部跳过。vitis 这类工具链生成的 makefile 里通常也已经带了 -MMD,所以你会看到 build 目录里一堆 .d 文件,那是标准的现代做法,不是私货。清理时记得把 .d 一起删掉,否则旧 .d 会把已删文件的位置牢牢记住,下次 include 时报 No such file。
5. 排查:make 报错与预期不符的五个现场
报错信息单独拿出来讲,是因为 make 的报错和它的行为一样“按规则来”:每条报错都指向某个具体的语法或依赖问题,看懂了就是最快的排查路径。下面是我在项目里反复遇到的五个现场,按“现象 → 原因 → 解决”展开。
5.1 现象:missing separator,或命令没有执行
现象:make 直接报Makefile:3: *** missing separator. Stop.,检查发现命令前确实有缩进,但用的不是 tab。
原因:makefile 的规则命令必须用 tab 字符开头。编辑器把 tab 自动展开成 4 个空格的场景很常见,尤其从 Windows 复制 makefile 到 Linux 时,行尾还带着 CRLF,CRLF 会让 tab 判断失效。
解决:在编辑器里关闭“tab 转空格”,重新把缩进敲成真 tab。用cat -A Makefile检查:tab 显示为^I,行尾的 CRLF 显示为^M$,看到^I就是对的。这个检查命令我几乎每次排错都会先用一遍。
提示:如果 makefile 是从 Windows 拷过来的,先执行
sed -i 's/\r$//' Makefile去掉 CRLF,再处理缩进,否则改完 tab 还会被行尾的 \r 干扰。
5.2 现象:make clean 报 up to date,命令一条没跑
现象:执行 make clean 后,rm 命令一条都没出现,屏幕上只有make: 'clean' is up to date.
原因:clean 被当成了文件。你没有写 .PHONY 声明,而磁盘上恰好存在一个名为 clean 的文件,make 认为目标已最新,跳过命令。
解决:在 makefile 顶部补.PHONY: clean或.PHONY: all clean install,把动作类目标全部声明为伪目标。这是最便宜的后悔药:不写可能永远不触发,一旦目录里出现同名文件就必炸。
5.3 现象:No rule to make target,或 makefile 整体找不到
现象:make: *** No rule to make target 'src/foo.c', needed by 'build/foo.o'. Stop.;在空目录里直接运行 make 则报No targets specified and no makefile found。
原因:前者是依赖图里某个文件缺失——路径写错、文件被移动、wildcard 没匹配到任何内容;后者是在没有 makefile 的目录里直接调用了 make,找不到构建脚本。
解决:先用$(info sources=$(sources))在 makefile 里打印变量,确认 wildcard 是否真的展开了路径。直觉上src/*.c在文件不存在时会原样保留,导致后续规则匹配不上。路径确认无误后,检查当前工作目录:make 只在当前目录找 Makefile,子目录里的构建请用make -C subdir进入目录,或者在顶层 makefile 里用$(MAKE) -C subdir递归调用。
5.4 现象:改了头文件,程序行为纹丝不动
现象:include/api.h 里改了一个宏定义,重新 make,日志显示链接被执行了,但 .o 文件没有重新编译,程序行为看不出变化。
原因:规则里没有声明 .o 对头文件的依赖。make 只比较了 .c 的时间戳,头文件不在依赖列表里,自然不触发重编。
解决:补CFLAGS += -MMD -MP并-include所有 .d 文件,也就是第 4.3 节那套。验证方式是把一个 .d 文件打开看内容,确认里面包含 .h。如果这是现存工程,修改 makefile 后跑一次make clean && make,让依赖文件全部重新生成,否则旧的 .d 不会自动补齐。
5.5 现象:嵌套 make 的 error 1,vitis 场景的经典报错
现象:日志尾部出现类似make[2]: *** [makefile:18: libs] error 1,但往上翻,真正的编译错误信息要么很靠前,要么被吞掉了,你根本不知道是谁先失败的。
原因:这是嵌套 make 的典型形态。make[2] 表示这是第二层子 make,makefile:18 指子 makefile 第 18 行,libs 是该 makefile 里的目标名,error 1 表示规则里的命令返回了退出码 1。真正出错的是那条编译或链接命令——可能是交叉编译工具链没找到、库路径不对、或者某个源文件语法错误,但多层嵌套会让真实信息淹没在前面的日志里。
解决:别盯着最后一行,往回找“第一次出现”的 error、fatal error、undefined reference。打开子 makefile 的第 18 行看它调了哪条命令,然后在命令行手动执行一遍那条命令(把变量展开后的实际值代入),立刻能看到完整报错。还可以给 make 传-w参数,它会在进入和离开每个子目录时打印信息,帮你建立“当前在哪个目录执行”的坐标感。如果错误被完全静默,检查有没有 @ 前缀或重定向把输出丢了,临时去掉 @ 再跑一次。
6. 调试三板斧:让 make 把话说清楚
6.1 make -n 与 $(info):预演和打印
最常用的两个调试姿势。make -n 只打印命令不执行,让你在动手前看清 make 打算跑什么、跳过什么——比任何代码注释都可靠。想进一步看依赖决策过程,用make --debug=v,它会逐条说明某个目标为什么过期、为什么跳过。日志很吵,但非常诚实。
在 makefile 里埋打印也很有用,$(info sources=$(sources))在解析阶段就会输出,适合确认变量值是否符合预期。我习惯在写完 wildcard 或 patsubst 之后立刻加一行打印,确认展开结果,确认完删掉。
6.2 验证增量编译:touch 一个头文件试试水
构建完成后,手动touch include/api.h,再跑 make。预期行为是只有真正包含这个头文件的 .o 被重编,其余全部 up to date。如果你看到的和预期不符,说明依赖没接全。这是我的最终验收动作,比翻看一整屏日志更直观。
这套调试方法陪我处理过几乎所有 makefile 问题。养成“先 -n 再动手”的习惯之后,多数“玄学”都会现出原形——make 的行为其实是完全确定的,觉得它不可理喻的时候,多半是依赖或路径没写对。完整的 GNU make 中文手册按“变量、函数、规则”三个目录去查即可,不用顺序读。希望帮到你。
本文还有配套的精品资源,点击获取