Makefile Tutorial;Makefile cheatsheet
介绍
makefile 定义了一系列规则来指定编译规则,make 是解释 makefile 中指令的命令工具。例子:来自 GNU 的 make 手册,指导编译、链接。
- 如果这个工程没有编译过,那么我们的所有 C 文件都要编译并被链接。
- 如果这个工程的某几个 C 文件被修改,那么我们只编译被修改的 C 文件,并链接目标程。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 C 文件,并链接目标程序。
Makefile 规则
核心概念
- 目标(Target):需要生成的文件或执行的命令标签。
- 依赖(Dependencies/prerequisites):目标构建所需的文件或其他目标。
- 命令(Recipe):生成目标的具体 Shell 命令。
- 规则(Rule):目标、依赖和命令的组合,格式为:
target ... : prerequisites ...
command ...其中包含文件依赖关系,即 target 目标文件依赖于 prerequisites 中的文件,生成规则定义在 command 中,也即 prerequisites 中若有一个以上文件比 target 文件要新的话,command 定义命令就被执行。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
# 必须以 tab 开头
edit : $(objects)
cc -o edit $(objects)
# 隐式规则
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
...
# 伪目标
.PHONY = clean
clean :
rm edit main.o $(objects)make 找第一个目标文件 target,作为最终目标文件。make 会一层层地寻找文件依赖关系,直到编译出第一个目标文件,若出现错误,则直接退出报错。
Makefile 的组成
- 显示规则:说明如何生成一个或多个目标文件
- 隐晦规则:make 有自动推导功能,可以简略地写 Makefile
- 变量定义:变量一般为字符串
- 文件指示:在一个 Makefile 中引用另一个 Makefile,类似 include;根据某些情况指定 Makefile 有效部分,类似 if;定义一个多行命令
- 注释:行注释#
- | Ignore errors |
@ | Don’t print command |
+ | Run even if Make is in ‘don’t execute’ mode |
特殊目标
1. .PHONY:声明伪目标
告诉 Make 某个目标不对应实际文件,而是代表一组命令的名称。即使存在同名文件,Make 也会强制执行对应的命令。
.PHONY: 目标1 目标2 ….PHONY: clean all
all: program
clean:
rm -f program *.oall和clean是伪目标,不对应实际文件。- 执行
make clean时,无论是否存在名为clean的文件,都会执行rm命令。 - 避免冲突:若目录中存在名为
clean的文件,make clean会认为目标已更新,导致命令不执行。 - 提高效率:伪目标不检查文件时间戳,直接执行命令。
2. .DEFAULT_GOAL:指定默认目标
当用户直接运行 make 而不指定目标时,明确告诉 Make 应该执行哪个目标。
.DEFAULT_GOAL: 目标名称.DEFAULT_GOAL: all
all: program
@echo "Building program…"- 直接执行
make时,等同于执行make all。
传统上,Make 默认执行 Makefile 中第一个目标。但使用 .DEFAULT_GOAL 可以:
- 显式指定默认目标,提高可读性。
- 将默认目标放在文件任意位置(不必是第一个)。
.DEFAULT_GOAL: all
.PHONY: all clean
all: program
@echo "Building program…"
clean:
@echo "Cleaning up…"
rm -f program *.o- 直接运行
make时,默认执行all。 all和clean始终被视为未更新,确保命令执行。
注意事项
-
兼容性
.DEFAULT_GOAL是 GNU Make 的特性,部分旧版 Make 可能不支持。传统项目通常通过将all作为第一个目标实现相同效果。 -
伪目标命名 伪目标应避免与实际文件名冲突,常见命名如:
all,clean,install,test,check等。 -
执行顺序
.DEFAULT_GOAL的声明位置不影响默认目标的执行顺序,它只决定 “哪个目标是默认的”。
总结
.PHONY:确保目标命令始终执行,避免与文件冲突。.DEFAULT_GOAL:明确指定默认目标,增强 Makefile 的可维护性。
Make 运行
make 命令执行后有三个推出码:
- 0 - 成功执行
- 1 - make 运行出现错误,返回 1.
- 2 - 如果使用了
-q选项,并且 make 使得一些目标不需要更新 make 寻找默认的 Makefile 执行,也可以指定文件make -f xx.mk,指定目标。
检查规则
-n --just-print --dry-run --recon不执行,只打印,不管目标是否更新,把规则和连带规则下的命令打印不执行。-t --touch把目标文件的时间更新,但不更改目标文件。make 假装编译目标,但不真正编译,只是把目标变成已编译的状态。-q --question找目标,如果目标存在则不输出也不编译;若不存在,则打印出错信息。-W <file> --what-if=<file> --assume-new=<file> --new-file=<file>这个参数需要指定一个文件。一般是是源文件 (或依赖文件),Make 会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和-n参数一同使用,来查看这个依赖文件所发生的规则命令。-b -m忽视版本兼容性-B --always-make认为所有目标都需要更新-C <dir> --directory=<dir>指定读取 makefile 目录,如果有多个,则前后叠加。--debug[=<options>]输出 make 的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是输出最简单的调试信息。下面是<options>的取值: a — 也就是 all,输出所有的调试信息。(会非常的多) b —— 也就是 basic,只输出简单的调试信息。即输出不需要重编译的目标。 v —— 也就是 verbose, 在 b 选项的级别之上。输出的信息包括哪个 makefile 被解析,不需要被重编译的依赖文件 (或是依赖目标) 等。 i —— 也就是 implicit,输出所以的隐含规则。 j —— 也就是 jobs,输出执行规则中命令的详细信息,如命令的 PID、返回码等。 m —— 也就是 makefile,输出 make 读取 makefile,更新 makefile,执行 makefile 的信息。-d --debug=a-e --environment-overrides环境变量覆盖 makefile 中定义的变量值-f=<file> --file=<file> --makefile=<file>指定需要执行的 makefile-h --help-i --ignore-errors执行时忽视所有错误-I <dir> --include-dir=<dir>指定一个倍包含 makefile 的搜索目标,可以有多个-j [<jobsnum] --jobs[=<jobsnum>]指定同时运行命令的个数,加速运行-k --keep-going出错了不停止-l <load> --load-average[=<load>] --max-load[=<load>]指定 make 运行命令的负载-o <file> --old-file=<file> --assume-old=<file>不重新生成指定 file,即使这个目标依赖文件新于它。-p --print-data-base输出 makefile 所有数据,包括所有规则和变量。-q --question不运行命令也不输出,仅检查指定目标是否需要更新,0 为需要,2 为有错误。-r --no-builtin-rules进制使用任何隐含规则-R --no-builtin-variables进制 make 使用任何作用于变量上的隐含规则-s --silent --quiet运行时不输出命令的输出-S --no-keep-going --stop取消-k选项-w --print-directory输出运行 makefile 之前后之后的信息--no-print-directory禁止-w选项-W <file> --what-if=<file> --new-file=<file> --assume-file=<file>假定目标 file 需要更新--warn-undefined-variables只要 make 发现未定义的变量,输出警告。
规则
规则包含依赖关系和生成目标的方法,Makefile 只有一个最终目标,一般第一个目标为最终目标。
通配符
make 支持三种 *, ?, […],和 UNIX 的 B-Shell 相同。
自动生成依赖
大多数 C/C++ 编译器都支持一个“-M”选项,即自动寻找源文件中包含的头文件,生成依赖关系。不过使用 GNU 的 C/C++ 编译器,需要用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
cc -MM main.c
main.o : main.c defs.hGNU 组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个“name. c”的文件都生成一个“name. d”的 Makefile 文件。[. d] 文件中就存放对应 [. c] 文件的依赖关系。于是,我们可以写出 [. c] 文件和 [. d] 文件的依赖关系,并让 make 自动更新或自成 [. d] 文件,并把其包含在我们的主 Makefile 中。如下:
%.d: %.c
@mkdir -p $(dir $@); \
rm -f $@; \
$(CC) -MM $< >$@.tmp; \
sed 's,\($*\)\.o[ :]*,1.o $@ : ,g' < $@.tmp > $@; \
rm -f $@.tmp
# $@.xxxx 表示随即编号,这里$@.tmp可以任意替代
main.o main.d: main.c defs.h这个规则的意义是,所有 [.d] 依赖于 [.c],首先删除目标,然后为每个目标 $< 生成用 $@ 命名的文件。然后在编译器生成的依赖中加入 [.d],[.d] 文件会自动更新、生成。使用 include 添加其他文件。
sources = foo.c bar.c
include $(sources:.c=.d)$(@:.o=.d) 是一个变量替换表达式,用于将目标文件(.o)的名称转换为对应的依赖文件(.d)的名称。
-
$@:自动变量,表示当前规则的目标文件(如build/main.o)。 -
:%.o=%.d:替换模式,将.o后缀替换为.d。 示例: -
若
$@为build/main.o,则$(@:.o=.d)会变为build/main.d。 在编译 C/C++ 代码时,.d文件用于记录源文件的头文件依赖关系,确保头文件修改时能触发重新编译。可以 使用patsubst替代
模式规则
模式规则类似一个一般的规则,只是规则中,目标定义需要 % 字符,它可以表达一个或多个任意字符。依赖中也可以包含。
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@命令
每条规则中的命令和操作系统 Shell 的命令行是一致的。make 的 命 令 默 认 是 被“/bin/sh”——UNIX 的标准 Shell 解释执行的。除非特别指定一个其它的 Shell。
make 会按顺序执行命令,每条命令的开头必须以 [Tab] 键开头,除非命令是紧跟在依赖规则后面的分号后的。在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以 Tab 键开头的,make 会认为其是一个空命令。make 针对每条命令,都会创建一个独立的 Shell 环境。如果要让上一条命令的结果应用在下一条命令时,应该把这两条命令写在一行上,使用分号分隔这两条命令。
Define
为相同的命令序列定义一个变量。define 的第一行执行程序、第二行更改名称。
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
foo.c : foo.y
$(run-yacc)条件
ifeq ($(CC), gcc)
…
else
…
endif使用 ifeq ifneq 判断。
ifdef 判断变量是否为空,
$(if CONDITION, THEN-PART, ELSE-PART)是 Makefile 的条件函数。
变量
变量的命名字可以包含字符、数字、下划线 (可以是数字开头),但不应该含有“:”、“#”、“=”或是空字符 (空格、回车等)。变量是大小写敏感的。还有一些特殊的自动化变量。
变量声明需要赋初值,使用时加上 $,最好用小括号或花括号包括。如果要使用 $ 字符,需要用 $$ 表示。变量会在使用它的地方精确地展开
定义
a=,变量可以嵌套,而且没有定义顺序
foo = $(bar)
bar = $(ugh)
ugh = Huh?
all:
echo $(foo)
> Huh?- 使用
:=立即赋值,如果使用了其他变量必须已经定义。 ?=表示如果没有定义则使用这个定义,否则不执行+=可以给变量追加值。若变量未定义,则+=变为a=,若用a=定义,则继承;若用:=定义,则同样:=赋值define定义,变量的值可以包含函数、命令、文字, 或是其它变量。而且命令同样需要Tab开头。!=右边为一条 shell 命令,返回赋值。如var != echo "hello"。- 如果有变量是通常 make 的命令行参数设置的,那么 Makefile 中对这个变量的赋值会被忽略。如果你想在 Makefile 中设置这类参数的值, 那么, 你可以使用“override”指示符。
override <var> = <val>
override <var> := <val>
override <var> += <val>
override define foo
bar
endef高级用法
- 替换
$(var:a=b),将变量var中所有以a字符结尾的a替换成b$(var:%.o=%.*c*)使用静态模式,要求模式中有一个%匹配。 - 变量可以看成变量
x = $(y)
y = z
z = hello
a := $($(x))这里替换后 a:=$($(y))=$(z)=hello。
这种替换可以结合函数、字符的替换,
环境变量
make 运行时的系统环境变量可以在 make 开始运行时被载入 Makefile 中,但是若 Makefile 已经定义类这个变量,或者这个变量由 make 命令行带入,系统的环境变量的值将被覆盖。(make 指定了 -e,系统环境变量会覆盖 Makefile)
当 make 嵌套调用时,上层 Makefile 定义的变量会以系统环境变量的方式传递给下层 Makefile。默认只有命令行设置的变量会被传递。定义在文件中的变量,向下层 Makefile 传递,需要使用 export 关键字声明。
内置变量
在 Makefile 中,GNU Make 提供了许多内置变量(也称为自动变量和预定义变量),这些变量可以简化规则编写并提供对当前构建环境的访问。以下是一些常见的内置变量分类及示例:
一、自动变量(针对每个规则生效)
自动变量在规则的命令中使用,引用与当前目标和依赖相关的文件名称。
| 变量名 | 描述 |
|---|---|
$@ | 当前目标的名称。 示例: target: dep 中,$@ 为 target。 |
$< | 第一个依赖文件的名称。 示例: target: a.o b.o 中,$< 为 a.o。 |
$^ | 所有依赖文件的名称,以空格分隔,会去除重复项。 示例: target: a.o b.o a.o 中,$^ 为 a.o b.o。 |
$+ | 所有依赖文件的名称,以空格分隔,保留重复项。 示例: target: a.o b.o a.o 中,$+ 为 a.o b.o a.o。 |
$? | 所有比目标更新的依赖文件,以空格分隔。常用于只处理更新过的文件。 |
$* | 目标文件的主文件名(不含扩展名)。 示例:目标为 foo.o,$* 为 foo。 |
$(@D) | 当前目标的目录部分(不含文件名)。 示例:目标为 src/foo.o,$(@D) 为 src。 |
$(@F) | 当前目标的文件部分(不含目录)。 示例:目标为 src/foo.o,$(@F) 为 foo.o。 |
$(<D) | 第一个依赖的目录部分。 |
$(<F) | 第一个依赖的文件部分。 |
$%仅当目标是函数库文件时,表示规则中的目标成员名$*表示目标模式中%及其之前的部分$(@D) $(@F)表示$@的目录部分和文件部分,如dir/foo.o中分别是dir foo.o。同样有$(*D) $(%D) $(<D) $(^D) $(+D) $(?D)
二、预定义变量(由 Make 自动设置)
这些变量提供对编译器、链接器和其他工具的默认设置。
1. 编译器和工具
| 变量名 | 默认值 | 描述 |
|---|---|---|
CC | cc | C 编译器命令 |
CXX | g++ | C++ 编译器命令 |
AS | as | 汇编器命令 |
LD | ld | 链接器命令 |
AR | ar | 归档工具(如创建静态库) |
RM | rm -f | 删除文件命令 |
MAKE | make | 递归调用 make 的命令 |
2. 编译和链接选项
| 变量名 | 描述 |
|---|---|
CFLAGS | C 编译器的选项(如 -O2 -Wall) |
CXXFLAGS | C++ 编译器的选项 |
CPPFLAGS | C/C++ 预处理器的选项(如 -I 头文件路径) |
LDFLAGS | 链接器的选项(如 -L 库路径) |
LIBS | 链接时需要的库(如 -lm) |
ASFLAGS | 汇编器的选项 |
ARFLAGS | 归档工具的选项(如 rcs) |
三、其他内置变量
| 变量名 | 描述 |
|---|---|
MAKEFILE_LIST | 当前 make 读取的所有 Makefile 文件名,按读取顺序排列。 示例: Makefile config.mk |
MAKECMDGOALS | 命令行指定的目标列表。 示例:执行 make clean all 时,MAKECMDGOALS 为 clean all。 |
SHELL | 执行命令的 shell,默认为 /bin/sh。 |
VPATH | 搜索依赖文件的目录列表(以冒号分隔)。 |
PWD | 当前工作目录(等同于 shell 中的 $PWD)。 |
四、变量修改修饰符
这些修饰符可用于处理文件名,通常与自动变量结合使用。
| 修饰符 | 示例 | 描述 |
|---|---|---|
% | $(var:%=foo_%) | 模式替换。 示例:若 var=a.o b.o,则结果为 foo_a.o foo_b.o。 |
D | $(dir src/foo.c) | 取目录部分。结果为 src/。 |
F | $(notdir src/foo.c) | 取文件部分。结果为 foo.c。 |
S | $(suffix foo.c) | 取后缀。结果为 .c。 |
B | $(basename foo.c) | 取 basename(不含后缀)。结果为 foo。 |
六、查看所有内置变量
你可以通过以下命令查看 GNU Make 的所有内置变量及其默认值:
make -p -f /dev/null | grep '^[A-Z]' | sort这会输出 Make 的内置规则和变量定义,帮助你了解可用的内置变量。
函数
调用:(<func> <arg>)
字符串处理
$(wildcard *.c)获取所有.c文件$(subst <from>, <to>, <text>)把字符串中进行替换,返回被替换的字符串。$(pathsubst <pattern>, <replacement>, <text>)查找并替换。可以包含%通配符,如果替换前后都包含,则匹配的部分不变。$(strip <string>)去除开头结尾空格。$(findstring <find>, <in>)查找字符串,入如果找到则返回,否则返回空。$(filter <pattern …>, <text>)以模式过滤,保留符合模式的单词$(filter-out <pattern …>, <text>)反过滤,去除符合模式的单词$(sort <list>)升序排列,并且去掉相同的单词$(word <n>, <text>)取单词中第 n 个单词(从 1 开始),若超出索引返回空$(wordlist <s>, <e>, <text>)从字符串中取出 s 到 e 的子列表$(words <text>)统计单词个数$(firstword <text>)取首个单词,也可以用 word 实现
文件名操作
下面的每个参数字符串都会当成一个或一系列文件名
$(dir <names…>)取目录名$(notdir <names…>)取文件名,非目录部分$(suffix <names…>)取文件名后缀$(basename <names…>)取出各个文件名前缀,即取出后缀$(addsuffix <suffix>, <names…>)添加后缀$(addprefix <prefix>, <names…>)添加前缀$(join <list1>, <list2>)把两个字符串依次添加
循环
$(foreach <var>, <list>, <text>)把list中的单词逐一取出放到var指定变量,然后执行text,返回的字符串以空格分隔。注意var是一个临时变量$(if <condition>, <then-part>, <else-part>)返回两个部分
其它
$(call <expr>, <param1>, <param2>,…)用来创建新的参数化的函数。expr读取变量如$(1), $(2),返回值是call的返回值。$(origin <var>)返回变量的来源$(shell <expr>)执行 shell 命令$(error <text>)产生错误,返回错误信息$(warning <text>)输出错误信息,并不退出$(eval <text>)允许此函数新定义一个 makefile 下的结构,包含变量、目标、隐式或者显示的规则。eval 函数的参数会被展开,然后再由 makeifle 进行解析。也就是说,eval 函数会被 make 解析两次,第一次是对 eval 函数的解析,第二次是 make 对 eval 参数的解析。 示例:
1 define func
2 $1:
3 cc $2 -o $$@
4 endef
5
6 $(eval $(call func,main,main.c))执行结果:
cc main.c -o mainMisc
MAKEFILE_LIST
当使用 sed 's/vitis-run/\/opt\/vitis/g' Makefile | make -f - 和直接执行 make 时,MAKEFILE_LIST 变量的内容会有明显不同。下面为你详细分析:
1. MAKEFILE_LIST 变量的作用
在 Makefile 里,MAKEFILE_LIST 是一个内置变量,它的作用是按顺序记录 make 所读取的所有 Makefile 文件的名称。具体来说:
- 第一个元素一般是默认的 Makefile(像
Makefile、makefile或者GNUmakefile)。 - 后续元素是通过
include指令包含进来的其他 Makefile 文件。 - 变量中的文件名称之间用空格分隔。
2. 直接执行 make 时的 MAKEFILE_LIST
假设项目里有以下几个文件:
Makefile
config.mk
tools.mk并且 Makefile 中包含这样的内容:
include config.mk
include tools.mk
all:
@echo "MAKEFILE_LIST: $(MAKEFILE_LIST)"当你直接执行 make 时,MAKEFILE_LIST 的输出通常是:
MAKEFILE_LIST: Makefile config.mk tools.mk这是因为 make 会按照下面的顺序读取文件:
- 首先读取默认的 Makefile(也就是
Makefile)。 - 接着读取
Makefile中通过include指令包含的config.mk和tools.mk。
3. 执行 sed '…' | make -f - 时的 MAKEFILE_LIST
当你使用 sed 's/vitis-run/\/opt\/vitis/g' Makefile | make -f - 命令时:
make -f -表示从标准输入读取 Makefile 内容,而不是从文件中读取。- 这种情况下,标准输入没有文件名,所以
MAKEFILE_LIST中对应的位置会显示为-(表示标准输入)。
示例输出:
MAKEFILE_LIST: - config.mk tools.mk详细说明:
- 第一个元素
-代表从标准输入读取的 Makefile 内容(也就是经过sed处理后的内容)。 - 后续的
config.mk和tools.mk是通过include指令包含进来的文件,这和直接执行make时是一样的。
4. 为什么会有这种差异?
这种差异主要是由 make -f - 的工作方式决定的:
- 直接执行
make:make 会读取磁盘上实际存在的 Makefile 文件,所以MAKEFILE_LIST中记录的是真实的文件名。 - 使用
make -f -:make 从标准输入读取内容,这些内容没有关联的文件名,因此用-来表示。
5. 实际应用中的影响
- 相对路径问题:如果 Makefile 中使用了
$(dir $(lastword $(MAKEFILE_LIST)))这样的表达式来获取 Makefile 所在的目录,在使用make -f -时会得到-,而不是实际的目录路径,这可能会导致错误。 - 调试方面:在调试时,
MAKEFILE_LIST中显示-可以帮助你判断 Makefile 是从标准输入读取的,而不是从文件中读取的。
6. 如何验证?
你可以在 Makefile 中添加一个目标来打印 MAKEFILE_LIST:
print-makefiles:
@echo "MAKEFILE_LIST: $(MAKEFILE_LIST)"然后分别执行以下两个命令进行验证:
# 直接执行 make
make print-makefiles
# 通过 sed 管道执行 make
sed 's/vitis-run/\/opt\/vitis/g' Makefile | make -f - print-makefiles总结
| 执行方式 | MAKEFILE_LIST 示例 | 说明 |
|---|---|---|
make | Makefile config.mk tools.mk | 显示实际的文件名 |
sed '…' | make -f - | - config.mk tools.mk | 标准输入用 - 表示 |
这种差异在编写需要依赖 Makefile 路径的 Makefile 时需要特别注意,建议优先使用变量或者环境变量来指定路径,而不是依赖 MAKEFILE_LIST。