# Makefile 介绍
# Makefile 的规则
<target> : <prerequisites>
[tab] <commands>
...
2
3
target
目标,可以是执行文件,也可以是一个标签。prerequisites
生成target
所依赖的文件或target
。command
该target
要执行的命令。
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
# Makefile 里有什么?
Makefile 里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出要 生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则。由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由 make 所支持的。
- 变量的定义。在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语 言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
- 注释。Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用 # 字符,这个就像 C/C++
中的 // 一样。如果你要在你的 Makefile 中使用 # 字符,可以用反斜杠进行转义,如:
\#
。 最后,还值得一提的是,在 Makefile 中的命令,必须要以 Tab 键开始。
# 引用其他 Makefile
include <filename>
filename 可以是当前操作系统 Shell 的文件模式(可以包含路径和通配符)。
make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。
-I
选项指定查找目录。
# 环境变量
如果你的当前环境中定义了环境变量 MAKEFILES ,那么,make 会把这个变量中的值做一个类似于 include 的动作。
建议不要使用。
# Make 的工作方式
GNU 的 make 工作时的执行步骤如下:
- 读入所有的 Makefile。
- 读入被 include 的其它 Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
# 书写规则
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
一般来说,定义在 Makefile中的第一条规则中的目标将被确立为最终的目标。
# 规则语法
<target> : <prerequisites>
[tab] <commands>
...
2
3
target
目标,可以是执行文件,也可以是一个标签。prerequisites
生成target
所依赖的文件或target
。command
该target
要执行的命令。
如果命令太长,你可以使用反斜杠(\ )作为换行符。
# 通配符
make 支持三个通配符:*
,?
和 ~
。
~
表示用户的 $HOME
目录。
如果要将通配符用于变量中,需要这样写:
objects := $(wildcast *.o)
列出一确定文件夹中的所有 .c
文件:
objects := $(wildcard *.c)
列出文件对应的 .o
文件:
$(patsubst %.c %.o, $(wildcard, *.c))
objects := $(patsubst %.c,%.o, $(wildcard *.c))
foo: $(objects)
cc -o foo $(objects)
2
3
# 文件搜寻
当 make 需要去找寻文件的依赖关系时,你可以在文件前加上路径,但最好的方法是把一个路径告诉 make,让 make 在自动去找。
Makefile 文件中的特殊变量 VPATH 就是完成这个功能的,如果没有指明这个变量,make 只会在当前的目录中去找寻依赖文件和目标文件。
VPATH = src:../headers
上面的定义指定两个目录,“src”和“../headers”,make 会按照这个顺序进行搜索。目录由“冒号”分隔。
vpath 关键字
vpath <pattern> <directories>
为符合模式<pattern>
的文件指定搜索目录 <directories>
。
vpath <pattern>
清除符合模式<pattern>
的文件的搜索目录。
vpath
清除所有设置好的文件搜索目录。
vpath 使用方法中的 <pattern>
需要包含 % 字符。% 的意思是匹配零或若干字符
vpath %.h ../headers
该语句表示,要求 make 在“../headers”目录下搜索所有以 .h 结尾的文件。
# 伪目标
“伪目标”并不是一个文件,只是一个标签,需要显式指明才能让其生效。为了避免和文件重名,需要使用特殊标记:
.PHONY: clean
clean:
rm -f *.o temp
2
3
可以利用伪目标一次性生成多个目标:
.PHONY : all
all : prog1 prog2 prog3
prog1 : prog1.o utils.o
cc -o prog1 prog1.o utils.o
prog2 : prog2.o
cc -o prog2 prog2.o
prog3 : prog3.o sort.o utils.o
cc -o prog3 prog3.o sort.o utils.o
2
3
4
5
6
7
8
9
10
11
伪目标也可以是依赖:
.PHONY: cleanall cleanobj cleandiff
cleanall: cleanobj cleandiff
rm program
cleanobj:
rm *.o
cleandiff:
rm *.diff
2
3
4
5
6
7
8
9
10
# 多目标
Makefile 的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。于是我们就能把其合并起来。
bigoutput littleoutput: text.g
generate text.g -$(subst output,,$@) > $@
2
等价于:
bigoutoutput: text.g
generate text.g -big > bigoutput
littleoutput: text.g
generate text.g -little > littleoutput
2
3
4
5
# 静态模式
静态目标可以更容易地定义多目标的规则。
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
2
3
targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-pattern 是指明了 targets 的模式,也就是的目标集模式。
prereq-patterns 是目标的依赖模式,它对 target-pattern 形成的模式再进行一次依赖目标的定义。
例子:
CC = gcc
CFLAGS = -Wall -Werror -g -O0
OBJS := foo.o bar.o
all: $(OBJS)
$(OBJS): %.o : %.c
$(CC) $(CFLAGS) -o $@ -c $<
2
3
4
5
6
7
8
9
%.o
指明我们的目标从 OBJS
中获取所有以 .o
结尾的目标,%.c
则取 %
匹配的内容,加上 .c
的后缀。
依赖目标就变为 foo.c bar.c
上面例子等价于
CC = gcc
CFLAGS = -Wall -Werror -g -O0
foo.o : foo.c
$(CC) -o $@ $(FLAGS) -c $<
bar.o : bar.c
$(CC) -o $@ $(FLAGS) -c $<
2
3
4
5
6
7
8
例子二:
files = foo.elc bar.o lose.o
$(filter %.o, $(files)) : %.o : %.c
$(CC) -c $(CFLAGS) $< -o $@
$(filter %.o, $(files)) : %.o : %.c
emacs -f batch-byte-compile $<
2
3
4
5
6
7
$(filter %.o $(files))
表明调用 makefile 的 filter
函数,过滤 $(files)
集。只要其中模式为 %.o
的内容。
# 自动生成依赖性
太复杂,跳过,以后用到再学。
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
2
3
4
5
这个规则的意思是,所有的 .d
文件依赖于 .c
文件,rm -f $@
的意思是删除所有的目标,也就是
.d
文件,第二行的意思是,为每个依赖文件 $<
,也就是 .c
文件生成依赖文件,$@
表示模式 %.d
文件,
如果有一个 C 文件是 name.c,那么 % 就是 name ,$$$$ 意为一个随机编号,第二行生成的文件有可能
是“name.d.12345”,第三行使用 sed 命令做了一个替换
这个模式要做的事就是在编译器生成的依赖关系中加入 .d 文件的依赖,即把依赖关系
main.o : main.c defs.h
转成
main.o main.d : main.c defs.h
# 书写命令
@
屏蔽回显。
-n
参数,只打印,不执行。
-s
silent 只执行不打印。
# 命令执行
如果需要上一条的命令应用在下一条命令时,写在同一行,并用 ;
分隔。
exec:
cd /home/hchen; pwd
2
# 命令出错
命令执行完后,make 会检测每个命令的返回码。
为了忽略命令的出错,可在命令行前加上一个 -
号。
clean:
-rm -f *.o
2
# 嵌套执行 Make
举例:
subsystem:
$(MAKE) -C subdir
2
表示进入 subdir
目录,执行 make
命令。
传递变量给下级的 Makefile
。
export <variable ...>
variable = value
export variable
2
传递所有变量,只要一个 export
就行。
有两个变量,一个是 SHELL ,一个是 MAKEFLAGS ,这两个变量不管你是否 export,其总是要传递到下层 Makefile 中
如果我们在执行 make
时,有参数,那么 MAKEFLAGS
变量会将这些参数传递到下层 Makefile,这是系统级的环境变量。
但是 make 命令中的有几个参数并不往下传递,它们是 -C , -f , -h, -o 和 -W
如果不想往下传参数,可以这样写:
subsystem:
cd substr && $(MAKE) MAKEFLAGS=
2
# 定义命令包
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
foo.c : foo.y
$(run-yacc)
2
3
4
5
6
7
# 使用变量
类似于 C 中的宏。
自定义变量建议不要全大写,首字母大写,避免和系统变量冲突。
# 变量基础
使用变量需要用 $()
包裹。
Objects = program.o foo.o utils.o
program : $(Objests)
cc -o program $(Objects)
$(Objects) : defs.h
2
3
4
5
6
例如:
foo = c
prog.o : prog.$(foo)
$(foo)$(foo) -$(foo) prog.$(foo)
2
3
4
# 变量中的变量
延时变量表示:使用该变量的时候,才展开该变量,并确定该变量的值
立即变量表示:定义的时候就已经确定了该变量的值
=
递归赋值。
:=
赋值。
碰到变量需要展开,如果是=
,则先往后面查找,如果是:=
,则先往前面查找
?=
如果没有被赋值才赋值
# 变量的高级用法
变量值的替换
$(var:a=b)
把变量 "var" 中所有以 'a' 字符串结尾的 'a' 替换为 'b' 。
foo := a.o b.o c.o
bar := $(foo:.o=.c)
2
把变量的值当作变量:
$($(x))
静态模式替换:
foo := a.o b.o c.o
bar := $(foo: %.o=%.c)
2
# 追加变量值
+=
以:=
作为赋值符。
# 函数
函数调用:
$(<function> <arguments>)
参与用逗号分隔。
# 字符串处理函数
subst
$(sbust <from>,<to>,<text>)
$(subst ee,EE,feet on the street)
patsubst
$(patsubst <pattern>,<replacement>,<text>)
$(patsubst %.c,%.o,x.c bar.c )
# 文件名操作函数
dir
$(dir <names...>)
$(dir src/foo.c hacks)
返回值是 src/ ./
。
notdir
$(not dir src/foo.c hacks)
返回值是 foo.c hacks
。
wildcard
$(patsubst %.c,%.o,$(wildcard *.c))
,首先使用“wildcard”函数获取工作目录下的.c文件列表;之后将列表中所有文件名的后缀.c替换为.o。
# Make 的运行
-f
指定 make。
# 常见伪目标:
- all:所有目标。
- clean:清理。
- install;安装编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
- print:列出所有改变过的源文件。
- tar:把源代码打包备份成一个 tar 文件。
- dist:创建一个压缩文件,一般是
xz
文件。 - TAGS:更新所有的目标,以备完整编译使用。
- check 和 test,测试用。
# 选项
-n
打印不执行。-B
重编译所有目标。-C
指定读取 makefile 的目录。如果有多个“-C”参数,make 的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录。如:“make -C ~hchen/test -C prog”等价于“make -C hchen/test/prog”。
# 隐含规则
例如,把 .c 文件编译成 .o 文件这一规则,你根本就不用写出来,make 会自动推导出这种规则,并生成我们需要的 .o 文件。
make 调用的隐含规则是,把 .o 的目标的依赖文件置成 .c ,并使用 C 的编译命令 cc –c $(CFLAGS) foo.c
来生成 foo.o 的目标。
隐含规则一览:
- 编译 C 程序的隐含规则。
<n>.o
的目标的依赖目标会自动推导为<n>.c
,并且其生成命令是$(CC) –c $(CPPFLAGS) $(CFLAGS)
- 编译 C++ 程序的隐含规则。
<n>.o
的目标的依赖目标会自动推导为<n>.cc
或是<n>.C
,并且其生成命令是$(CXX) –c $(CPPFLAGS) $(CXXFLAGS)
。(建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C )
# 命令变量
- AR : 函数库打包程序。默认命令是 ar
- AS : 汇编语言编译程序。默认命令是 as
- CC : C 语言编译程序。默认命令是 cc
- CXX : C++ 语言编译程序。默认命令是 g++
- CO : 从 RCS 文件中扩展文件程序。默认命令是 co
- CPP : C 程序的预处理器(输出是标准输出设备)。默认命令是 $(CC) –E
- FC : Fortran 和 Ratfor 的编译器和预处理程序。默认命令是 f77
- GET : 从 SCCS 文件中扩展文件的程序。默认命令是 get
- LEX : Lex 方法分析器程序(针对于 C 或 Ratfor)。默认命令是 lex
- PC : Pascal 语言编译程序。默认命令是 pc
- YACC : Yacc 文法分析器(针对于 C 程序)。默认命令是 yacc
- YACCR : Yacc 文法分析器(针对于 Ratfor 程序)。默认命令是 yacc –r
- MAKEINFO : 转换 Texinfo 源文件(.texi)到 Info 文件程序。默认命令是 makeinfo
- TEX : 从 TeX 源文件创建 TeX DVI 文件的程序。默认命令是 tex
- TEXI2DVI : 从 Texinfo 源文件创建军 TeX DVI 文件的程序。默认命令是 texi2dvi
- WEAVE : 转换 Web 到 TeX 的程序。默认命令是 weave
- CWEAVE : 转换 C Web 到 TeX 的程序。默认命令是 cweave
- TANGLE : 转换 Web 到 Pascal 语言的程序。默认命令是 tangle
- CTANGLE : 转换 C Web 到 C。默认命令是 ctangle
- RM : 删除文件命令。默认命令是 rm –f
# 命令参数的变量
- ARFLAGS : 函数库打包程序 AR 命令的参数。默认值是 rv
- ASFLAGS : 汇编语言编译器参数。(当明显地调用 .s 或 .S 文件时)
- CFLAGS : C 语言编译器参数。
- CXXFLAGS : C++ 语言编译器参数。
- COFLAGS : RCS 命令参数。
- CPPFLAGS : C 预处理器参数。(C 和 Fortran 编译器也会用到)。
- FFLAGS : Fortran 语言编译器参数。
- GFLAGS : SCCS “get”程序参数。
- LDFLAGS : 链接器参数。(如:ld )
- LFLAGS : Lex 文法分析器参数。
- PFLAGS : Pascal 语言编译器参数。
- RFLAGS : Ratfor 程序的 Fortran 编译器参数。
- YFLAGS : Yacc 文法分析器参数。
# 自动化变量
$@
: 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@ 就是匹配于目标中模式定义的集合。$%
: 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是 foo.a(bar.o)- 那么,$%
就是 bar.o ,$@ 就是 foo.a 。如果目标不是函数库文件(Unix 下是 .a ,Windows下是 .lib ),那么,其值为空。$<
: 依赖目标中的第一个目标名字。如果依赖目标是以模式(即 % )定义的,那么 $< 将是符合模式的一系列的文件集。注意,其是一个一个取出来的。$?
: 所有比目标新的依赖目标的集合。以空格分隔。$^
: 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。$+
: 这个变量很像 $^ ,也是所有依赖目标的集合。只是它不去除重复的依赖目标。$*
: 这个变量表示目标模式中 % 及其之前的部分。如果目标是 dir/a.foo.b ,并且目标的模式是a.%.b ,那么,$* 的值就是 dir/foo 。这个变量对于构造有关联的文件名是比较有效。如果目标中没有模式的定义,那么 $* 也就不能被推导出,但是,如果目标文件的后缀是 make 所识别的,那么 $* 就是除了后缀的那一部分。例如:如果目标是 foo.c ,因为 .c 是 make 所能识别的后缀名,所以,$* 的值就是 foo 。这个特性是 GNU make 的,很有可能不兼容于其它版本的 make,所以,你应该尽量避免使用 $* ,除非是在隐含规则或是静态模式中。如果目标中的后缀是 make 所不能识别的,那么 $* 就是空值。
打包:
lib : foo.o bar.o lose.o win.o
ar r lib $?
2
四个变量($@
、$<
、$%
、$*
)在扩展时只会有一个文件,另外三个是文件列表。
$(@D)
表示 $@ 的目录部分(不以斜杠作为结尾),如果 $@ 值是 dir/foo.o ,那么 $(@D) 就是 dir ,而如果 $@ 中没有包含斜杠的话,其值就是 . (当前目录)。$(@F)
表示 $@ 的文件部分,如果 $@ 值是 dir/foo.o ,那么 $(@F) 就是 foo.o ,$(@F) 相当于函数$(notdir $@) 。$(*D)
,$(*F)
和上面所述的同理,也是取文件的目录部分和文件部分。对于上面的那个例子,$(*D)
返回dir
,而$(*F)
返回 foo$(%D)
, $(%F)分别表示了函数包文件成员的目录部分和文件部分。这对于形同 archive(member) 形式的目标中的 member 中包含了不同的目录很有用。$(<D)
,$(<F)
分别表示依赖文件的目录部分和文件部分。$(^D)
, $(^F)分别表示所有依赖文件的目录部分和文件部分。(无相同的)$(+D)
, $(+F)分别表示所有依赖文件的目录部分和文件部分。(可以有相同的)$(?D)
, $(?F)分别表示被更新的依赖文件的目录部分和文件部分。
最后想提醒一下的是,对于 $<
,为了避免产生不必要的麻烦,我们最好给 $ 后面的那个特定字符
都加上圆括号,比如,$(<)
就要比 $<
要好一些。
# 参考
陈皓-跟我一起写 Makefile (opens new window)
自动生成依赖关系
foreach
函数。
filter
函数
filter-out
函数。
ifneq
通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 Makefile 中, 头文件改动后,自动确认需要重新编译的文件。
gcc -M des 获取 des 的完整依赖关系
gcc -MM des 获取目标des 的部分依赖关系(-E 仅对依赖关系做初步解析)
gcc -MM 命令可以自动生成源文件对头文件的依赖关系,且剔除掉库里面的头文件
makefile 中的 include 关键字
类似 C 中的 include,将其他文件的内容原封不动的搬入当前文件
include 是按次序来载入文件,最先载入的 .d 文件中的目标会成为默认目标。
GNU Make手册建议使用正则表达式显式创建依赖文件
GNU make 官方手册建议写法:
all: main
main: main.o stack.o maze.o
gcc $^ -o $@
clean:
-rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
-include $(sources:.c=.d)
%.d: %.c
set -e; rm -f $@; \
$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
clean:
rm -rf $(TARGET) *.o *.d *.d.*
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
自动处理头文件的依赖关系 (opens new window) Makefile-自动生成依赖-示例分析 (opens new window)
objs := Hello.o Fun.o
Hello : $(objs)
gcc -o $@ $^
#判断是否存在依赖文件
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))
#包含依赖文件
ifneq ($(dep_files),)
include $(dep_files)
endif
%.o:%.c
#编译.c文件,并且把依赖的文件写入.$@.d
gcc -Wp,-MD,.$@.d -c -o $@ $<
clean:
rm -rf *.o Hello
#增加伪目标
.PHONY : clean
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 自动以最大线程编译
sudo make -j $(nproc)