本文总结自陈皓老师《跟我一起写MakeFile》
Makefile的基本概念
Makefile是一种用于自动化构建程序的工具,主要用于管理源代码的编译和链接过程。它依赖于规则(rules)来定义文件的依赖关系及生成方式,通常用于C/C++项目,但也支持其他语言。
Makefile的核心语法
Makefile由一系列规则(rules)组成,每条规则包含目标(target)、依赖(prerequisites)和命令(commands)。基本格式如下:
target: prerequisites
commands
目标通常是生成的文件(如可执行文件或目标文件),依赖是生成目标所需的文件,命令是实际执行的Shell命令。
hello:hello.o
gcc hello.o -o hello
hello.o:hello.S
gcc -c hello.S -o hello.o
hello.S:hello.i
gcc -S hello.i -o hello.S
hello.i:hello.c
gcc -E hello.c -o hello.i
注:越是接近目标文件的命令,就越是要写在前面。因为程序是按照递归的方式进行依赖文件查找的,看到第一行有一个没见过的依赖文件,就往下一行进行查找,以此类推。
Makefile的变量与宏
Makefile支持变量定义,以提高灵活性和可维护性。变量可以通过=、:=、?=和+=定义,例如:
= 操作符(递归展开)
在 Makefile 中,= 是递归展开赋值操作符。通过 = 赋值的变量会在每次被引用时动态展开。这意味着变量的值可以包含其他变量的引用,而这些引用的变量会在最终使用时才被展开。递归展开可能会导致变量值在后续被修改时影响之前引用的结果。
VAR_A = $(VAR_B)
VAR_B = Hello
all:
@echo $(VAR_A) # 输出 Hello,因为 VAR_B 的值在最终使用时才展开
:= 操作符(简单展开)
:= 是简单展开赋值操作符。与 = 不同,:= 会在变量定义时就立即展开右侧的表达式,后续对变量的引用不会重新展开。因此,:= 更适合用于需要固定值的场景,避免递归展开带来的不确定性。
VAR_B = World
VAR_A := $(VAR_B)
VAR_B = Hello
all:
@echo $(VAR_A) # 输出 World,因为 VAR_A 的值在定义时就已经固定
?= 操作符(条件赋值)
?= 是条件赋值操作符。只有当变量未被定义时,才会将右侧的值赋给变量。如果变量已经被定义(即使为空值),?= 不会覆盖原有值。这种操作符常用于为变量提供默认值。
VAR_A ?= Default
all:
@echo $(VAR_A) # 输出 Default
VAR_A = Override
all:
@echo $(VAR_A) # 输出 Override
+= 操作符(追加赋值)
+= 是追加赋值操作符。它用于在变量已有的值后追加新内容,类似于字符串拼接。如果变量之前未被定义,+= 的行为与 = 相同。追加的内容可以是字符串或空格分隔的列表。
VAR_A = Hello
VAR_A += World
all:
@echo $(VAR_A) # 输出 Hello World
在代码或脚本中,-include <filename> 通常用于指示编译器或解释器在遇到错误时继续执行而不中断。这种语法常见于 Makefile 或其他构建系统中。
在 Makefile 中添加 -include
如果需要在 Makefile 中包含文件并忽略错误,可以在合适的位置加入:
-include filename.mk
这样,即使 filename.mk 不存在或包含错误,Makefile 仍会继续执行。
通配符概述
Makefile中的通配符用于匹配文件名或路径,简化文件操作和规则定义。主要包括以下几种通配符:
*(星号)
匹配任意长度的字符(包括零个字符),通常用于文件名或扩展名匹配。
- 示例:
*.c匹配当前目录下所有.c文件。 - 注意:不匹配以
.开头的隐藏文件(如.gitignore),除非显式指定(如.*)。
?(问号)
匹配任意单个字符。
- 示例:
file?.txt匹配file1.txt、fileA.txt,但不匹配file10.txt。 - 用途:精确匹配文件名中某个位置的单个字符。
[...](字符集)
匹配方括号内任意一个字符。支持范围表示(如 [0-9])和否定(如 [^a-z])。
- 示例:
file[1-3].txt匹配file1.txt、file2.txt、file3.txt。 - 否定示例:
file[^0-9].txt匹配非数字结尾的文件名(如fileA.txt)。
%(百分号)
Makefile特有的模式匹配通配符,常用于规则定义和函数。
- 示例规则:
%.o: %.c $(CC) -c $^ -o $@ %.o : 任意的.o文件 %.c : 任意的.c文件 *.o :所有的.o文件 $^ :所有依赖文件 $@ :所有目标文件 $< :所有依赖文件的第一个文件 - 表示将任意
.c文件编译为同名.o文件。 - Makefile 中$^:表示所有依赖文件的列表,通常用于规则中。例如:
target: dep1 dep2 dep3 command $^ # 等价于 dep1 dep2 dep3 - Makefile 中$@:表示当前规则的目标文件名。例如:
output.txt: input.txt cp $< $@ # 等价于 cp input.txt output.txt - Makefile 中$<:表示第一个依赖文件。例如:
output.txt: input.txt config.txt cat $< > $@ # 等价于 cat input.txt > output.txt
~(波浪号)
匹配用户的主目录路径。
- 示例:
~/project展开为/home/username/project。 - 特殊形式:
~user匹配指定用户的主目录(如~root)。
{}(花括号)
生成多个可能的匹配项(逗号分隔),不是所有Make版本都支持。
- 示例:
file.{c,o}展开为file.c file.o。 - 用途:简化重复模式的列举。
条件判断
Makefile支持条件判断(如ifeq、ifneq、ifdef)和内置函数(如$(shell ...)、$(foreach ...)),用于实现动态逻辑。例如:
ifeq (是否相同)
libs_for_gcc = -lgnu
normal_libs =
foo: $(objects)
ifeq ($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(normal_libs)
endif
我们可以从上面的示例中看到三个关键字:ifeq、else 和 endif。ifeq 的意思表示条
件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括
号括起。else 表示条件表达式为假的情况。endif 表示一个条件语句的结束,任何一个条件
表达式都应该以 endif 结束。
当我们的变量$(CC)值是“gcc”时,目标 foo 的规则是:
foo: $(objects)
$(CC) -o foo $(objects) $(libs_for_gcc)
而当我们的变量$(CC)值不是“gcc”时(比如“cc”),目标 foo 的规则是:
foo: $(objects)
$(CC) -o foo $(objects) $(normal_libs)
ifdef(是否定义)
示例一:
bar =
foo = $(bar)
ifdef foo
frobozz = yes
else
frobozz = no
endif
示例二:
foo =
ifdef foo
frobozz = yes
else
frobozz = no
endif
第一个例子中,“$(frobozz)”值是“yes”,第二个则是“no”。
函数(字符串操作)
$(subst <from>,<to>,<text>)
名称:字符串替换函数——subst。
功能:把字串<text>中的<from>字符串替换成<to>。
返回:函数返回被替换过后的字符串。
示例:
$(subst ee,EE,feet on the street)
把“feet on the street”中的“ee”替换成“EE”,返回结果是“fEEt on the
strEEt”。
$(strip <string>)
名称:去空格函数——strip。
功能:去掉<string>字串中开头和结尾的空字符。
返回:返回被去掉空格的字符串值。
示例:
$(strip a b c )
把字串“a b c ”去到开头和结尾的空格,结果是“a b c”。
findstring
$(findstring <find>,<in>)
名称:查找字符串函数——findstring。
功能:在字串<in>中查找<find>字串。
返回:如果找到,那么返回<find>,否则返回空字符串。
示例:
$(findstring a,a b c)
$(findstring a,b c)
第一个函数返回“a”字符串,第二个返回“”字符串(空字符串)
$(filter <pattern...>,<text>)
名称:过滤函数——filter。
功能:以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。可以
有多个模式。
返回:返回符合模式<pattern>的字串。
示例:
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources))返回的值是“foo.c bar.c baz.s”。
函数(文件名操作)
dir
$(dir <names...>)
名称:取目录函数——dir。
功能:从文件名序列<names>中取出目录部分。目录部分是指最后一个反斜杠(“/”)之前
的部分。如果没有反斜杠,那么返回“./”。
返回:返回文件名序列<names>的目录部分。
示例: $(dir src/foo.c hacks)返回值是“src/ ./”。
suffix
$(suffix <names...>)
名称:取后缀函数——suffix。
功能:从文件名序列<names>中取出各个文件名的后缀。
返回:返回文件名序列<names>的后缀序列,如果文件没有后缀,则返回空字串。
示例:$(suffix src/foo.c src-1.0/bar.c hacks)返回值是“.c .c”。
basename
$(basename <names...>)
名称:取前缀函数——basename。
功能:从文件名序列<names>中取出各个文件名的前缀部分。
返回:返回文件名序列<names>的前缀序列,如果文件没有前缀,则返回空字串。
示例:$(basename src/foo.c src-1.0/bar.c hacks)返回值是“src/foo src-1.0/b
ar hacks”。
shell 函数
shell 函数也不像其它的函数。顾名思义,它的参数应该就是操作系统 Shell 的命令。
它和反引号“`”是相同的功能。这就是说,shell 函数把执行操作系统命令后的输出作为
函数返回。于是,我们可以用操作系统命令以及字符串处理命令 awk,sed 等等命令来生成
一个变量,如:
contents := $(shell cat foo)
files := $(shell echo *.c)
注意,这个函数会新生成一个 Shell 程序来执行命令,所以你要注意其运行性能,如果你的
Makefile 中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有
害的。特别是 Makefile 的隐晦的规则可能会让你的 shell 函数执行的次数比你想像的多得
多。
foreach 函数
foreach 函数和别的函数非常的不一样。因为这个函数是用来做循环用的,Makefile
中的 foreach 函数几乎是仿照于 Unix 标准 Shell(/bin/sh)中的 for 语句,或是 C-Shell
(/bin/csh)中的 foreach 语句而构建的。它的语法是:
$(foreach <var>,<list>,<text>)
这个函数的意思是,把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,
然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>
的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串
所组成的整个字符串(以空格分隔)将会是 foreach 函数的返回值。
所以,<var>最好是一个变量名,<list>可以是一个表达式,而<text>中一般会使用<var>
这个参数来依次枚举<list>中的单词。举个例子:
names := a b c d
files := $(foreach n,$(names),$(n).o)
上面的例子中,$(name)中的单词会被挨个取出,并存到变量“n”中,“$(n).o”每次
根据“$(n)”计算出一个值,这些值以空格分隔,最后作为 foreach 函数的返回,所以,
$(files)的值是“a.o b.o c.o d.o”。
注意,foreach 中的<var>参数是一个临时的局部变量,foreach 函数执行完后,参数
<var>的变量将不在作用,其作用域只在 foreach 函数当中。
隐晦规则
make 有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书 写 Makefile,这是由 make 所支持的。
make 很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必
要去在每一个[.o]文件后都写上类似的命令,因为,我们的 make 会自动识别,并自己推导
命令。
只要 make 看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果 make
找到一个 whatever.o,那么 whatever.c,就会是 whatever.o 的依赖文件。并且 cc -c
whatever.c 也会被推导出来,于是,我们的 makefile 再也不用写得这么复杂。我们的是新
的 makefile 又出炉了
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
这种方法,也就是 make 的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean
是个伪目标文件。
常见问题与调试技巧
Makefile常见错误包括缩进错误(命令必须用Tab开头)、变量未定义或路径问题。调试时可使用--debug选项或添加$(info ...)打印变量值。
现代替代方案与总结
尽管Makefile仍被广泛使用,现代工具如CMake、Bazel和Ninja提供了更高级的抽象和跨平台支持。了解Makefile的原理有助于掌握这些工具的核心思想。
Makefile核心语法详解
731

被折叠的 条评论
为什么被折叠?



