《跟我一起写 Makefile》 读书笔记

《跟我一起写Makefile》1 是 陈皓 发表在其 CSDN 博客上的系列文章。该系列文章翻译整理自 GNU Make Manual ,一直受到读者的推荐,是很多人学习Makefile的首选文档。

Makefile 里有什么

  1. 显式规则
    • 显式规则说明了如何生成一个或多个目标文件。这是由 Makefile 的书写者明显指出要 生成的文件、文件的依赖文件和生成的命令。
  2. 隐晦规则
    • 由于我们的 make 有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Make- file,这是由 make 所支持的。
  3. 变量的定义
    • 在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像你 C 语 言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示
    • 在一个 Makefile 中引用另一个 Makefile,就像 C 语言中的 include 一样
    • 根据某些情况指定 Makefile 中的有效部分,就像 C 语言中的预编译 #if 一样;
    • 定义一个多行的命令
  5. 注释。
    • Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用 # 字符,这个就像 C/C++ 中的 // 一样。如果你要在你的 Makefile 中使用 # 字符,可以用反斜杠进行转义,如:# 。

在 Makefile 中的命令,必须要以 Tab 键开始。

引用其它的 Makefile

1
include <filename>

make 的工作方式

  1. 读入所有的 Makefile。
  2. 读入被 include 的其它 Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

书写规则

1
2
3
targets : prerequisites
command
...

通配符

  • *
  • ?
  • ~

文件搜寻

1
2
3
4
# 特殊变量 VPATH
VPATH = src:../headers
# make 的“vpath”关键字
vpath %.h ../headers

伪目标

“伪目标”并不是一个文件,只是一个标签,由于“伪目标” 不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。

1
2
3
.PHONY : clean
clean :
-rm *.o temp

多目标

1
2
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@

静态模式

1
2
3
<targets ...> : <target-pattern> : <prereq-patterns ...>    
<commands>
...
  • targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
  • target-pattern 是指明了 targets 的模式,也就是的目标集模式。
  • prereq-patterns 是目标的依赖模式,它对 target-pattern 形成的模式再进行一次依赖目标的定义。
1
2
3
4
5
6
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@

自动生成依赖性

1
cc -M main.c

一个模式规则来产生 .d 文件:

1
2
3
4
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ rm -f $@.$$$$

引入别的 Makefile 文件

1
2
sources = foo.c bar.c
include $(sources:.c=.d)

书写命令

显示命令

1
@echo 正在编译 XXX 模块......

命令出错

在 Makefile 的命令行前加一个减号 - (在 Tab 键之 后),标记为不管命令出不出错都认为是成功的

1
2
clean:
-rm -f *.o

嵌套执行 make

  • =
1
2
3
4
export variable = value
# ==
variable = value
export variable
  • :=
1
2
3
4
export variable := value
# ==
variable := value
export variable
  • +=
1
2
3
4
export variable += value
# ==
variable += value
export variable

定义命令包

1
2
3
4
define run-yacc yacc
$(firstword $^)
mv y.tab.c $@
endef

使用

1
2
foo.c : foo.y
$(run-yacc)

使用变量

变量的基础

需要给在变量名前加上 $ 符号,但最好用小括号 () 或是大括号 {} 把变量给包括起来。

如果你要使用真实的 $ 字符,那么你需要用 $$ 来表示。

变量中的变量

  • $<:第一个依赖文件;
  • $@:目标;
  • $^:所有不重复的依赖文件,以空格分开

= 使用后面定义

1
2
3
4
5
objects = program.o foo.o utils.o
program : $(objects)
cc -o program $(objects)

$(objects) : defs.h

我们执行“make all”将会打出变量 $(foo) 的值是 Huh?

($(foo) 的值是 \((bar) ,\)(bar) 的值 是 \((ugh) ,\)(ugh) 的值是 Huh? )

把变量的真实值推到后面来定义

1
2
FLAGS = $(include_dirs) -O
include_dirs = -Ifoo -Ibar

不好的地方:递归定义

:= 前面的变量不能使用后面的变量

1
2
3
4
5
6
x := foo
y := $(x)
bar x := later
# ==
y := foo bar
x := later

前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。

1
FOO ?= bar

定义空格

1
2
nullstring :=
space := $(nullstring) # end of the line

nullstring 是一个 Empty 变量,其中什么也没有,而我们的 space 的值是一个空格

如果 FOO 没有被定义过,那么变量 FOO 的值就是“bar”,如果 FOO 先前被定义过,那么这条语将什么也不做,其等价于:

1
2
3
ifeq ($(origin FOO), undefined)
FOO = bar
endif

+= 追加变量值

1
2
3
4
5
objects = main.o foo.o bar.o utils.o
objects += another.o
# ==
objects = main.o foo.o bar.o utils.o
objects := $(objects) another.o

所不同的是,用 += 更为简洁。

变量高级用法

变量值的替换

1
2
foo := a.o b.o c.o
bar := $(foo:.o=.c)

另外一种变量替换的技术是以“静态模式”

1
2
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

把变量的值再当成变量

1
2
3
x=y
y=z
a := $($(x))

override 指示符

1
2
override <variable>; = <value>;
override <variable>; := <value>;

多行变量 define 关键字

1
2
3
4
define two-lines
echo foo
echo $(bar)
endef

环境变量

  1. CFLAGS 环境变量
    • 全局
  2. Makefile 中定义了 CFLAGS
    • 局部

目标变量

1
2
3
<target ...> : <variable-assignment>;

<target ...> : overide <variable-assignment>

模式变量

1
%.o : CFLAGS = -O

使用条件判断

1
2
3
4
5
6
7
8
9
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

更简洁一些:

1
2
3
4
5
6
7
8
9
10
libs_for_gcc = -lgnu
normal_libs =
ifeq ($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif

foo: $(objects)
$(CC) -o foo $(objects) $(libs)

语法

条件表达式

1
2
3
<conditional-directive>
<text-if-true>
endif
1
2
3
4
5
<conditional-directive>
<text-if-true>
else
<text-if-false>
endif

if

koijm,l;[p;l,]

使用函数

函数调用,很像变量的使用,也是以 $ 来标识的,其语法如下:

1
2
3
$(<function> <arguments>)
# 或者
${<function> <arguments>}

Demo

1
2
3
4
5
comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

字符串处理函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# 字符串替换
$(subst <from>,<to>,<text>)
$(subst ee,EE,feet on the street)
# 模式字符串替换
$(patsubst <pattern>,<replacement>,<text>)
$(patsubst %.c,%.o,x.c.c bar.c)
# 去空格函数
$(strip <string>)
$(strip a b c )
# 查找字符串
$(findstring <find>,<in>)
$(findstring a,a b c)
# 过滤
$(filter <pattern...>,<text>)
sources := foo.c bar.c baz.s ugh.h
foo: $(sources)
cc $(filter %.c %.s,$(sources)) -o foo
# 反过滤
$(filter-out <pattern...>,<text>)
# 排序
$(sort <list>)
# 取单词
$(word <n>,<text>)
$(word 2, foo bar baz) # bar
# 取单词串
$(wordlist <ss>,<e>,<text>)
# 单词个数统计
$(words <text>)
$(words foo bar baz) # 3
# 首单词
$(firstword <text>)

# 应用
# 搜索路径来指定编译器对头文件的搜索路径参数 CFLAGS
override CFLAGS += $(patsubst %,-I%,$(subst :, ,$(VPATH)))

文件名操作函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
# 目录
$(dir <names...>)
$(dir src/foo.c hacks) # src/./
# 取文件
$(notdir <names...>)
$(notdir src/foo.c hacks) # foo.chacks
# 后缀
$(suffix <names...>)
$(suffix src/foo.c src-1.0/bar.c hacks) # .c .c
# 取前缀
$(basename <names...>)
$(basename src/foo.c src-1.0/bar.c hacks) # src/foos rc-1.0/bar hacks
# 加后缀
$(addsuffix <suffix>,<names...>)
$(addsuffix .c, foo bar) # foo.c bar.c
# 加前缀
$(addprefix <prefix>,<names...>)
$(addprefix src/, foo bar) # src/foo src/bar
# 连接
$(join <list1>,<list2>)
$(join aaa bbb,111 222 333) # aaa111 bbb222 333
# foreach
$(foreach <var>,<list>,<text>)
names := a b c d
files := $(foreach n,$(names),$(n).o)
# if
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
# call
$(call <expression>,<parm1>,<parm2>,...,<parmn>)
reverse = $(1) $(2)
# call 函数提供参数时,最安全的做法是去除所有多余的空格
foo = $(call reverse,a,b)
# origin 变量是哪里来的
$(origin <variable>)
# <variable> 是变量的名字,不应该是引用。所以你最好不要在 <variable> 中使用 $ 字符
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf, gag, etc.
endif
endif
# shell == ``
contents := $(shell cat foo)
files := $(shell echo *.c)
# 控制 make 的函数
$(error <text ...>)
$(warning <text ...>)
# demo 1
ifdef ERROR_001
$(error error is $(ERROR_001))
endif
# demo 2
ERR = $(error found an error!)
.PHONY: err
err: $(ERR)

make 的运行

make 的退出码

make 命令执行后有三个退出码:

  1. 0 表示成功执行。
  2. 1 如果 make 运行时出现任何错误,其返回 1。
  3. 2 如果你使用了 make 的“-q”选项,并且 make 使得一些目标不需要更新,那么返回 2。

指定 Makefile

1
make –f subdir.mk

指定目标

1
2
.PHONY: all
all: prog1 prog2 prog3 prog4

GNU 这种开源软件

  • all: 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。
  • clean: 这个伪目标功能是删除所有被 make 创建的文件。
  • install: 这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。
  • print: 这个伪目标的功能是例出改变过的源文件。
  • tar: 这个伪目标功能是把源程序打包备份。也就是一个 tar 文件。
  • dist: 这个伪目标功能是创建一个压缩文件,一般是把 tar 文件压成 Z 文件。或是 gz 文件。
  • TAGS: 这个伪目标功能是更新所有的目标,以备完整地重编译使用。
  • checktest: 这两个伪目标一般用来测试 makefile 的流程。

检查规则

  • -n, --just-print, --dry-run, --recon 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试 makefile 很有用处。
  • -t, --touch 这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make 假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
  • -q, --question 这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出, 当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
  • -W , --what-if=, --assume-new=, --new-file= 这个参数需要指定一个 文件。一般是是源文件(或依赖文件),Make 会根据规则推导来运行依赖于这个文件的命令,一般 来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。

另外一个很有意思的用法是结合 -p 和 -v 来输出 makefile 被执行时的信息。

make 的参数

-debug[=] 输出 make 的调试信息。它有几种不同的级别可供选择,如果没有参数,那就是 输出最简单的调试信息。下面是 的取值:

  • a: 也就是 all,输出所有的调试信息。(会非常的多)
  • b: 也就是 basic,只输出简单的调试信息。即输出不需要重编译的目标。
  • v: 也就是 verbose,在 b 选项的级别之上。输出的信息包括哪个 makefile 被解析,不需要被 重编译的依赖文件(或是依赖目标)等。
  • i: 也就是 implicit,输出所以的隐含规则。
  • j: 也就是 jobs,输出执行规则中命令的详细信息,如命令的 PID、返回码等。
  • m: 也就是 makefile,输出 make 读取 makefile,更新 makefile,执行 makefile 的信息。

隐含规则

1
2
3
4
5
6
7
foo : foo.o bar.o
cc –o foo foo.o bar.o $(CFLAGS) $(LDFLAGS)
# 没有必要写下下面的两条规则,可以隐含推出
foo.o : foo.c
cc –c foo.c $(CFLAGS)
bar.o : bar.c
cc –c bar.c $(CFLAGS)

隐含规则一览

默认的后缀列表是: .out, .a, .ln, .o, .c, .cc, .C, .p, .f, .F, .r, .y, .l, .s, .S, .mod, .sym, .def, .h, .info, .dvi, .tex, .texinfo, .texi, .txinfo, .w, .ch .web, .sh, .elc, .el

  1. 编译 C 程序的隐含规则。
    • .o 的目标的依赖目标会自动推导为 .c ,并且其生成命令是 $(CC) –c $(CPPFLAGS) $(CFLAGS)
  2. 编译 C++ 程序的隐含规则。
    • .o 的目标的依赖目标会自动推导为 .cc 或是 .C ,并且其生成命令是 $(CXX) –c $(CPPFLAGS) $(CFLAGS) 。(建议使用 .cc 作为 C++ 源文件的后缀,而不是 .C ) 编译 Pascal 程序的隐含规则。
  3. 编译 Pascal 程序的隐含规则。
    • .o 的目标的依赖目标会自动推导为 .p ,并且其生成命令是 $(PC) –c $(PFLAGS) 。
  4. 编译 Fortran/Ratfor 程序的隐含规则。
    • .o 的目标的依赖目标会自动推导为 .r 或 .F 或 .f ,并且其生成命令是: * .f $(FC) –c $(FFLAGS)
    • .F $(FC) –c $(FFLAGS) $(CPPFLAGS)
    • .f $(FC) –c $(FFLAGS) $(RFLAGS)
  5. 预处理 Fortran/Ratfor 程序的隐含规则。
    • .f 的目标的依赖目标会自动推导为 .r 或 .F 。这个规则只是转换 Ratfor 或有预处理的 Fortran 程序到一个标准的 Fortran 程序。其使用的命令是:
    • .F $(FC) –F $(CPPFLAGS) $(FFLAGS)
    • .r $(FC) –F $(FFLAGS) $(RFLAGS)
  6. 编译 Modula-2 程序的隐含规则。
    • .sym 的目标的依赖目标会自动推导为 .def ,并且其生成命令是:$(M2C) $(M2FLAGS) \((DEFFLAGS) 。<n>.o 的目标的依赖目标会自动推导为 <n>.mod ,并且其生成命令是:\)(M2C) $(M2FLAGS) $(MODFLAGS) 。
  7. 汇编和汇编预处理的隐含规则。
    • .o 的目标的依赖目标会自动推导为 .s ,默认使用编译器 as ,并且其生成命令是:$ (AS) \((ASFLAGS) 。<n>.s 的目标的依赖目标会自动推导为 <n>.S ,默认使用 C 预编译器 cpp ,并且 其生成命令是:\)(AS) $(ASFLAGS) 。
  8. 链接 Object 文件的隐含规则。
    • 目标依赖于 .o ,通过运行 C 的编译器来运行链接程序生成(一般是 ld ),其生成命令 是:$(CC) $(LDFLAGS) .o $(LOADLIBES) $(LDLIBS) 。这个规则对于只有一个源文件的工程 有效,同时也对多个 Object 文件(由不同的源文件生成)的也有效。例如如下规则:
      1
      x : y.o z.o
    • 并且 x.c 、y.c 和 z.c 都存在时,隐含规则将执行如下命令:
      1
      2
      3
      4
      5
      6
      7
      cc -c x.c -o x.o
      cc -c y.c -o y.o
      cc -c z.c -o z.o
      cc x.o y.o z.o -o x
      rm -f x.o
      rm -f y.o
      rm -f z.o
    • 如果没有一个源文件(如上例中的 x.c)和你的目标名字(如上例中的 x)相关联,那么,你最好写 出自己的生成规则,不然,隐含规则会报错的。
  9. Yacc C 程序时的隐含规则。
    • .c 的依赖文件被自动推导为 n.y (Yacc 生成的文件),其生成命令是:$(YACC) $(YFALGS)
      。(“Yacc”是一个语法分析器,关于其细节请查看相关资料)
  10. Lex C 程序时的隐含规则。
    • .c 的依赖文件被自动推导为 n.l(Lex 生成的文件),其生成命令是:$(LEX) $(LFALGS) 。(关 于“Lex”的细节请查看相关资料)
  11. Lex Ratfor 程序时的隐含规则。
    • .r 的依赖文件被自动推导为 n.l (Lex 生成的文件),其生成命令是:$(LEX) $(LFALGS) 。
  12. 从 C 程序、Yacc 文件或 Lex 文件创建 Lint 库的隐含规则。
    • .ln(lint 生成的文件)的依赖文件被自动推导为 n.c ,其生成命令是:$(LINT) $(LINTFALGS) $(CPPFLAGS) -i 。对于 .y 和 .l 也是同样的规则。

隐含规则使用的变量

例如,第一条隐含规则——编译 C 程序的隐含规则的命令是 $(CC) –c $(CFLAGS) $(CPPFLAGS) 。 Make 默认的编译命令是 cc ,如果你把变量 $(CC) 重定义成 gcc ,把变量 $(CFLAGS) 重定义成 -g ,那 么,隐含规则中的命令全部会以 gcc –c -g $(CPPFLAGS) 的样子来执行了。

我们可以把隐含规则中使用的变量分成两种:一种是命令相关的,如 CC ;一种是参数相的关,如 CFLAGS 。下面是所有隐含规则中会用到的变量:

关于命令的变量

  • 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 文法分析器参数。

隐含规则链

一个目标可能被一系列的隐含规则所作用。例如,一个 .o 的文件生成,可能会是先被
Yacc 的 [.y] 文件先成 .c ,然后再被 C 的编译器生成。我们把这一系列的隐含规则叫做“隐含规则链”。

使用 make 更新函数库文件

函数库文件的成员

1
archive(member)

这个不是一个命令,而一个目标和依赖的定义。一般来说,这种用法基本上就是为了 ar 命令来服务 的。如:

1
2
foolib(hack.o) : hack.o ar
cr foolib hack.o

函数库文件的后缀规则

1
2
3
4
.c.a:
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o

等效于

1
2
3
4
(%.o) : %.c
$(CC) $(CFLAGS) $(CPPFLAGS) -c $< -o $*.o
$(AR) r $@ $*.o
$(RM) $*.o

  1. https://github.com/seisman/how-to-write-makefile↩︎