159. Makefile

介绍

make 命令规则如下:

  • 如果这个工程没有编译过,那么我们的所有 c 文件都要编译并被链接。
  • 如果这个工程的某几个 c 文件被修改,那么我们只编译被修改的 c 文件,并链接目标程序。
  • 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的 c 文件,并链接目标程序。

​ 只要我们的 makefile 写得够好,所有的这一切,我们只用一个 make 命令就可以完成,make 命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译

makefile 的规则

1
2
3
4
target ... : prerequisites ...
recipe
...
...
  • target

    可以是一个 object file(目标文件) ,也可以是一个可执行文件,还可以是一个标签(label) 。对于标签这种特性,在后续的“伪目标”章节中会有叙述。

  • prerequisites

    生成该 target 所依赖的文件和/或 target。

  • recipe 该 target 要执行的命令(任意的 shell 命令) 。

​ 这是一个文件的依赖关系,也就是说,target 这一个或多个的目标文件依赖于 prerequisites 中的文件,其生成规则定义在 command 中。说白一点就是说: prerequisites 中如果有一个以上的文件比target文件要新的话,recipe所定义的命令就会被执行。

​ 依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的. 后续的 recipe 行定义了如何生成目标文件的操作系统命令, 一定要以一个 Tab 键作为开头. make 会比较 targets 文件和 prerequisites 文件的修改日期, 如果 prerequisites 文件的日期要比 targets 文件的日期要新, 或者target 不存在的话,那么, make 就会执行后续定义的命令.

1
2
3
clean:
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

​ 反斜杠()是换行符的意思。这样比较便于 makefile 的阅读.

​ clean 不是一个文件,它只不过是一个动作名字,有点像 C 语言中的 label 一样,其冒号后什么也没有,那么,make 就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在 make 命令后明显得指出这个 label 的名字。

make 是如何工作的

​ 在默认的方式下,也就是我们只输入 make 命令。那么,

  1. make 会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的**==第一个==**目标文件(target) ,在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
  3. 如果 edit 文件不存在,或是 edit 所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新, 那么,他就会执行后面所定义的命令来生成 edit 这个文件。
  4. 如果 edit 所依赖的 .o 文件也不存在,那么 make 会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。 (这有点像一个堆栈的过程) 5. 当然,你的 C 文件和头文件是存在的啦,于是 make 会生成 .o 文件,然后再用 .o 文件生成 make 的终极任务,也就是可执行文件 edit 了。

​ 在找寻的过程中, 如果出现错误, 比如最后被依赖的文件找不到,那么 make 就会直接退出,并报错, 而对于所定义的命令的错误, 或是编译不成功, make 根本不理。 make 只管文件的依赖性, 即, 如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。

​ 像 clean 这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行.

makefile 中使用变量

​ 为了 makefile 的易维护,在 makefile 中我们可以使用变量。makefile 的变量也就是一个字符串,理解成 C 语言中的宏可能会更好.

  • 定义/赋值: name = value
  • 使用: $(name)

让 make 自动推导

​ GNU 的 make 很强大,它可以自动推导文件以及文件依赖关系后面的命令. 只要 make 看到一个 .o 文件,它就会自动的把 .c 文件加在依赖关系中, 如:

  1. 如果 make 找到一个whatever.o ,那么 whatever.c 就会是 whatever.o 的依赖文件。
  2. cc -c whatever.c 也会被推导出来
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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)

.PHONY 表示 clean 是个伪目标文件

注意:

  • clean 的规则不要放在文件的开头
  • 不成文的规矩是——“clean 从来都是放在文件的最后” 。

Makefile 的文件名

​ 命名推荐:

  • Makefile
  • makefile
  • GNUmakefile(不推荐, 只能由GNU make识别)

​ 指定其他文件名, 即指定特定的 Makefile: 使用 -f--file, make -f Make.Solarismake --file Make.Linux. 如果你使用多条 -f--file 参数,你可以指定多个 makefile。

包含其它 Makefile

​ 在 Makefile 使用 include 指令可以把别的 Makefile 包含进来:

1
include <filenames>...
  • <filenames> 可以是当前操作系统 Shell 的文件模式(可以包含路径和通配符) 。

​ 在 include 前面可以有一些空字符, 但是绝不能是 Tab 键开始。 include<filenames> 可以用一个或多个空格隔开。 举个例子, 你有这样几个 Makefile: a.mk, b.mk, c.mk , 还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 bishbash ,那么,下面的语句:

1
include foo.make *.mk $(bar)

​ 等价于

1
include foo.make a.mk b.mk c.mk bish bash

​ make 命令开始时,会找寻 include 所指出的其它 Makefile,并把其内容安置在当前的位置。就好像 C/C++ 的 #include 指令一样。

查找顺序

​ 如果文件都没有指定绝对路径或是相对路径的话,make 会在当前目录下首先寻找,如果当前目录下没有找到,那么,make 还会在下面的几个目录下找:

  • 如果 make 执行时,有 -I--include-dir 参数,那么 make 就会在这个参数所指定的目录下去寻找。
  • 接下来按顺序寻找目录 <prefix>/include (一般是 /usr/local/bin ) 、/usr/gnu/include/usr/local/include/usr/include.
  • 环境变量 .INCLUDE_DIRS 包含当前 make 会寻找的目录列表。你应当避免使用命令行参数 -I 来寻找以上这些默认目录,否则会使得 make “忘掉”所有已经设定的包含目录,包括默认目录。

​ 如果你想让 make 不理那些无法读取的文件,而继续执行,你可以在 include 前加一个减号“-”:

1
-include <filenames>...

​ 其表示,无论 include 过程中出现什么错误,都不要报错继续执行。如果要和其它版本 make 兼容, 可以使用 sinclude 代替 -include.

书写规则

通配符与变量展开

1
objects = *.o

​ 上面这个例子,表示了通配符同样可以用在变量中。并不是说 *.o 会展开,不!objects 的值就是 *.o. Makefile 中的变量其实就是 C/C++ 中的宏。如果你要让通配符在变量中展开,也就是让 objects 的值是所有 .o 的文件名的集合,那么,你可以这样:

1
objects := $(wildcard *.o)

伪目标

​ “伪目标”并不是一个文件,只是一个标签,由于“伪目标”不是文件,所以 make 无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然, “伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。

​ 为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标” ,向 make 说明,不管是否有这个文件,这个目标就是“伪目标”.

​ 只要有这个声明,不管是否有“clean”文件,要运行“clean”这个目标,只有“make clean”这样.

​ 伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标” ,只要将其放在第一个。

有用的示例

​ 如果你的 Makefile 需要一口气生成若干个可执行文件,但你只想简单地敲一个 make 完事,并且,所有的目标文件都写在一个 Makefile 中,那么你可以使用“伪目标”这个特性:

1
2
3
4
5
6
7
8
9
all : prog1 prog2 prog3 
.PHONY : all

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

​ 解析:

  • Makefile 中的第一个目标会被作为其默认目标。
  • 我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于默认目标的特性是,==总是被执行的==, 但由于“all”又是一个伪目标,伪目标只是一个标签不会生成文件,所以不会有“all”文件产生。于是,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。
  • .PHONY : all 声明了“all”这个目标为“伪目标” 。 (注:这里的显式“.PHONY : all”不写的话一般情况也可以正确的执行,这样 make 可通过隐式规则推导出, “all”是一个伪目标,执行 make 不会生成“all”文件,而执行后面的多个目标。建议:显式写出是一个好习惯。

​ 目标也可以成为依赖。所以,伪目标同样也可成为依赖.

1
2
3
4
5
6
7
8
9
10
.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
rm program

cleanobj:
rm *.o

cleandiff:
rm *.diff

​ “make cleanall”将清除所有要被清除的文件。 “cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。

clean命令更加稳妥的做法:

1
2
3
.PHONY: clean
clean:
-rm edit $(objects)
  • .PHONY 表示: clean 是一个“伪目标”
  • rm 命令前面加了一个小减号表示: 也许某些文件出现问题,但不要管,继续做后面的事

自动生成依赖性

​ GNU 组织建议把编译器为每一个源文件的自动生成的依赖关系放到一个文件中,为每一个 name.c 的文件都生成一个 name.d 的 Makefile 文件,.d 文件中就存放对应 .c 文件的依赖关系。

​ 于是,我们可以写出 .c 文件和 .d 文件的依赖关系,并让 make 自动更新或生成 .d 文件,并把其包含在我们的主 Makefile 中,这样,我们就可以自动化地生成每个文件的依赖关系了。

​ 这里,我们给出了一个模式规则来产生 .d 文件:

1
2
3
4
5
%.d: %.c
@set -e; rm -f $@; \
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
rm -f $@.$$$$
  1. 所有的 .d 文件依赖于 .c 文件,rm -f $@ 的意思是删除所有的目标,也就是 .d 文件
  2. 第二行的意思是,为每个依赖文件 $< ,也就是 .c 文件生成依赖文件,$@ 表示模式 %.d 文件, 如果有一个 C 文件是 name.c,那么 % 就是 name ,$$$$ 意为一个随机编号,第二行生成的文件有可能是“name.d.12345”
  3. 第三行使用 sed 命令做了一个替换,关于 sed 命令的用法请参看相关的使用文档
  4. 第四行就是删除临时文件

执行命令

显示命令

不显示命令方式

  • @ 字符在命令行前: 不被make显示出来
  • 使用 -s--silent--quiet: 全面禁止命令的显示

只显示命令而不执行

​ make 执行时, 带入 make 参数 -n--just-print: 只是显示命令, 但不会执行命令 (适用于调试)

命令执行

  • 如果你要让上一条命令的结果应用在下一条命令时, 你应该使用分号分隔这两条命令

    1
    2
    3
    exec: 
    cd /home/hchen
    pwd

    输出 Makefile 所在目录

    1
    2
    exec: 
    cd /home/hchen; pwd

    输出: /home/hchen

shell

​ make 一般是使用环境变量 SHELL 中所定义的系统 Shell 来执行命令

忽略命令错误

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

  • 一个全局的办法是,给 make 加上 -i 或是 --ignore-errors 参数,那么,Makefile 中所有命令都会忽略错误

  • 如果一个规则是以 .IGNORE 作为目标的, 那么这个规则中的所有命令将会忽略错误

    1
    2
    3
    4
    .IGNORE: demo
    demo:
    mkdir demo
    @echo "===> folder demo is created."
  • make 的参数的是 -k 或是 –keep-going ,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则

定义命令包

​ 如果 Makefile 中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以 define 开始,以 endef 结束.

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

​ “run-yacc”是这个命令包的名字,其不要和 Makefile 中的变量重名。在 define 和 endef 中的两行就是命令序列。这个命令包中的第一个命令是运行 Yacc 程序,因为 Yacc 程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。还是把这个命令包放到一个示例中来看看.

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

​ 要使用这个命令包, 我们就好像使用变量一样。 在这个命令包的使用中, 命令包 “runyacc” 中的 $^ 就是 foo.y , $@ 就是 foo.c(有关这种以 $ 开头的特殊变量, 我们会在后面介绍) , make 在执行命令包时,命令包中的每个命令会被依次独立执行.

使用变量

基础

  • 定义/声明: name = val

  • 使用:

    • ${name}
    • $(name)

    建议使用 ${}, 这样会和shell脚本保持统一: $()在shell中是运行 () 中的命令

定义变量值为空格的变量

​ 定义一个变量,其值是一个空格,那么我们可以这样来:

1
2
nullstring :=
space := ${nullString} # end of the line

​ nullstring 是一个 Empty 变量,其中什么也没有,而我们的 space 的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个 Empty 变量来标明变量的值开始了, 而后面采用“#”注释符来表示变量定义的终止,这样,我们可以定义出其值是一个空格的变量。

注意: # 的应用

1
dir := /foo/bar    # directory to put the frobs in

​ dir 这个变量的值是“/foo/bar” ,后面还跟了 4 个空格

变量中的变量

​ 在定义变量的值时, 我们可以出差用其他变量来构造变量的值.

方法1: 简单的使用 “=” 号

  • 在 = 左侧是变量
  • 右侧是变量的值,右侧变量的值可以定义在文件的任何一处,也就是说,右侧中的变量不一定非要是已定义好的值,其也可以使用后面定义的值

例如:

1
2
3
4
5
6
foo = $(bar)
bar = $(ugh)
ugh = Huh?

all:
echo $(foo)

​ 执行“make all”将会打出变量 $(foo) 的值是 Huh? ($(foo) 的值是 $(bar) ,$(bar) 的值是 $(ugh) ,$(ugh) 的值是 Huh? )可见,变量是可以使用后面的变量来定义的

方法2: “:=” 操作符

​ 这种方法,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量。

例:

1
2
3
x := foo
y := $(x) bar
x := later

等价于

1
2
y := foo bar
x := later

?= 运算符

1
foo ?= bar

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

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

变量高级用法

变量值的替换

  • 格式:

    • $(var:a=b)
    • ${var:a=b}
  • 含义: 把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串.

    这里的“结尾”意思是“空格”或是“结束符”.

  • 静态模式

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

    依赖于被替换字串中的有相同的模式,模式中必须包含一个 % 字符,这个例子同样让 $(bar) 变量的值为“a.c b.c c.c”

把变量的值再当成变量

​ 例如:

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

​ a的值为 “z”

​ 可以使用多个变量来组成一个变量的名字

1
2
3
4
first_second = Hello
a = first
b = second
all = $($a_$b)
  • $a_$b 组成了“first_second”
  • $(all) 的值就是“Hello”

​ 把变量的值再当成变量”这种技术,同样可以用在操作符的左边:

1
2
3
4
5
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c)
define $(dir)_print
lpr $($(dir)_sources)
endef

追加变量值: “+=”

  • 如果变量之前没有定义过,那么,+= 会自动变成 =
  • 如果前面有变量定义,那么 += 会继承于前次操作的赋值符
    • 如果前一次的是 := ,那么 += 会以 := 作为其赋值符
    • 前次的赋值符是 = ,所以 += 也会以 = 来做为赋值

判断

语法

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

​ 在 <conditional-directive> 这一行上, 多余的空格是被允许的, 但是不能以 Tab 键作为开始 (不然就被认为是命令) 。而注释符 # 同样也是安全的。else 和 endif 也一样,只要不是以 Tab 键开始就行了。

conditional-directive

  • ifeq: 相同

  • ifneq: 不同

  • ifdef:

    1
    ifdef <variable-name>

    如果变量 <variable-name> 的值非空, 那到表达式为真。 否则, 表达式为假。 当然, <variable-name> 同样可以是一个函数的返回值

    ifdef 只是测试一个变量是否有值,其并不会把变量扩展到当前位置

  • ifndef: 与 ifdef 相反

函数

​ 在 Makefile 中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make 所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。

调用语法

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

  • $(<function> <arguments>)
  • ${<function> <arguments>}

​ 解释:

  • <function> 就是函数名
  • <arguments> 为函数的参数,参数间以逗号 , 分隔, 而函数名和参数之间以“空格”分隔。

多文件编译

使用宏

假设编译使用宏 DEBUG

  • 命令行传入: debug=1

  • Makefile:

    1
    2
    3
    ifeq ($(debug), 1)
    cflags += -DDEBUG
    endif

多文件makefile书写技巧

  1. 从里层开始写makefile, 然后调试里层的makefile

示例

  • 目录结构

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    .
    ├── Makefile
    ├── fs
    │   ├── Makefile
    │   ├── filesystems.cpp
    │   └── filesystems.h
    ├── include
    │   └── debug
    │   └── macros.h
    └── main.cpp
  • 父目录makefile

    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
    cc ?= g++

    cppflags = -Wall

    ifeq ($(debug), 1)
    cflags += -DDEBUG
    cppflags += -DDEBUG
    endif

    objs = main.o \
    fs/filesystems.o

    export cc cflags cppflags

    .PHONY: all clean fs_make

    all: fs_make demo

    demo: $(objs)
    $(cc) -o demo $(cppflags) $(objs)

    fs_make:
    $(MAKE) -C fs

    %.o: %.cpp
    g++ -c $(cppflags) $< -o $@

    clean:
    $(MAKE) clean -C fs
    rm -rf *.o
    rm -rf demo

  • 子目录makefile

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    cc ?= g++
    cflags ?= -Wall
    cppflags ?= -Wall

    cpp_src = $(wildcard *.cpp)
    c_src += $(wildcard *.c)

    cpp_objs = $(patsubst %.cpp,%.o,$(cpp_src))


    .PHONY: all clean
    all: $(cpp_objs)

    %.o: %.cpp
    g++ -c $(cppflags) $< -o $@

    %.o: %.c
    gcc -c $(cflags) $< -o $@

    clean:
    rm -rf *.o
    rm -rf demo