Makefile极简教程

关于Makefile的编写和make命令的简单应用,快速了解Makefile的编写规则,更多功能将在以后的使用中慢慢增加。

基本概念

makefile用于定义自动化编译命令make的规则,通过make命令,配合相关工程的makefile,可以让我们不再需要手动去一条条的编译和链接我们的源程序,直接通过执行一个命令就可以代替那些需要执行的编译命令。同时make命令会根据配置文件的状态(是否被修改)来确定是否需要重新编译该文件。
make命令的用法为:

1
make [TARGET ...]

即make后面跟需要执行的目标规则,目标规则便由makefile来定义。当make命令后面不跟TARGET时,make默认执行makefile中定义的第一条规则。当不通过-f参数手动指定make命令需要使用的规则文件时,make命令默认以以下顺序查找到第一个可用的规则文件:GNUmakefile, makefile, Makefile
所以我们使用make命令的首要工作就是编写一份包含有编译规则的makefile文件

makefile规则

规则原理

makefile文件由一系列的规则组成,每个规则由“目标名”+”依赖文件”+命令组成。形如:

1
2
3
4
5
6
7
8
9
10
11
12
target1 [target ...]: [component1 ...]
Tab ↹[command 1]
Tab ↹[command 2]
.
.
.
Tab ↹[command n]

target2 [target ...]: [component2 ...]
Tab ↹[command 1]
...
...

如上,包含了两条规则,通常目标名(即target)要唯一,命令必须以Tab开始(如果你的vim设置用4个空格替换Tab,可以通过在插入模式下使用ctrl+v+i插入一个Tab键),依赖文件(即component)将指定命令(command)中需要引用的文件,可以以变量的方式在command中引用传递的依赖文件,以实现复用。当然上述的component部分可以省略,当没有component部分是,称为伪目标,伪目标相当于可以让make帮助我们执行一个shell命令,例如经常会见到的clean
可以看到,一个规则中可以包含多个目标、多个依赖和多条命令。
#开头用于注释
一个简单的makefile,用于编译HelloWorld,为了增加复杂度,将输出函数单独放在一个hello.cpphello.h的文件中,main函数在main.cpp中,代码及makefile如下:
hello.h

1
2
3
4
5
6
#ifndef HELLO_H
#define HELLO_H

void print(void);

#endif /* !HELLO_H */

hello.cpp

1
2
3
4
5
6
7
#include "hello.h"
#include <iostream>

void print(void)
{
std::cout << "Hello world!" << std::endl;
}

main.cpp

1
2
3
4
5
6
7
#include "hello.h"

int main(void)
{
print();
return 0;
}

makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
#
# makefile
# blueyi, 2017-11-09 16:08
#

helloMain: main.cpp hello.cpp hello.h
g++ -o helloMain main.cpp hello.cpp hello.h

clean: # 伪目标
rm -rf helloMain

# vim:ft=make
#

可以看到后面有一个伪目标clean用于删除编译生成的文件。
注意:默认情况下make会查看当前目录的目标与依赖相比是否最新,如果是,则不再执行相应命令。这样就会导致如果文件夹下存在了clean文件,而其又没有依赖,那它的命令将永远不会执行。为了解决该问题,可以手动添加一句.PHONY: clean来指定clean为伪目标,这样make就总会执行该目标的命令
makefile的规则如此简洁,极易理解和使用,但当我们文件越来越多,并且我们想有更多的控制时,每次都要手动去添加文件就会大大增加工作量,此时可能通过宏来进行更好的实现。

宏(变量)

makefile中可以定义变量,有些地方称之为宏,实际上这里并没有很确切的边界,都是可以的,所以下面会混用这两个概念。定义方式形如:

1
MACRO = definition

通过$(MACRO_NAME)或者${MACRO_NAME}的方式来引用定义的宏。
通过简单的宏定义,我们可以将上述makefile改写如下,并加入中间文件obj文件的支持:
makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#
# makefile
# blueyi, 2017-11-09 16:08
#

cc = g++
prom = helloMain
deps = hello.h
obj = main.o hello.o

$(prom): $(obj)
$(cc) -o $(prom) $(obj)

main.o: main.cpp $(deps)
$(cc) -c main.cpp

hello.o: hello.cpp $(deps)
$(cc) -c hello.cpp

clean: # 伪目标
rm -rf helloMain hello.o main.o
# vim:ft=make
#

可以看到我们细分了依赖,将头文件作为hello.o和main.o的依赖,而可执行文件则依赖中间文件*.o

可以通过make命令修改makefile中定义的宏:

1
make MACRO="value" [MACRO="value" ...] TARGET [TARGET ...]

宏的值可以来源于shell命令,命令由”`”(Esc下面那个键)包围:

1
TIME = `date`

多个宏可以直接连接在一起组成一个宏。
举例如下(功能为将当前目录下的文件打包)。
makeTar

1
2
3
4
5
6
7
8
9
PACKAGE   = package
VERSION = ` date +"%Y.%m%d" `
ARCHIVE = $(PACKAGE)-$(VERSION)

dist:
# Notice that only now macros are expanded for shell to interpret:
# tar -cf package-`date +"%Y%m%d"`.tar

tar -cf $(ARCHIVE).tar .

执行:

1
make PACKAGE="study" -f makeTar

生成文件名为study-2017.1109.tar

利用宏可以让我们很方便的添加依赖关系、管理编译选项,但当我们的代码有了改动的时候依然会很麻烦,例如添加一些cpp和头文件后我们就不得不再次编辑makefile文件添加相关内容。通过自动变量可以在命令中引用target和component,以减少makefile工作量

自动变量|模式匹配|通配符

常用的内置宏如下:

  • $@ 引用target
  • $^ component部分以空格分隔的所有文件
  • $? 以空格分隔的,component部分比目标还要新的文件
  • $< 引用component中的第一个元素

更多自动变量参见GNU文档https://www.gnu.org/software/make/manual/html_node/Automatic-Variables.html#Automatic-Variables

模式匹配:

  • % 用于模式匹配,匹配连续的0个或更多个字符,可以理解为可用于匹配文件名,make会根据makefile中 第一个不带通配符的A目标构造规则对匹配模式进行展开
    而通配符*用于单个文件名的匹配

通配符
makefile中表示一个单一的文件名时可以使用通配符:*?[...],通配符的含义与shell中的含义完全相同,通配符可以在以下两种场合使用:

  1. 可以用在规则的目标、依赖中,此时make会自动将其展开;
  2. 可出现在规则的命令中,其展开是在shell在执行此命令时完成。

利用内置宏和匹配模式,上述HelloWorld的makefile可改写如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#
# makefile
# blueyi, 2017-11-09 16:08
#

cc = g++
prom = helloMain
deps = $(shell find ./ -name "*.h")
src = $(shell find ./ -name "*.cpp")
obj = $(src:%.c=%.o)

$(prom): $(obj)
$(cc) -o $(prom) $(obj)

%.o: %.cpp $(deps)
$(cc) -c $< -o $@

clean: # 伪目标
rm -rf helloMain hello.o main.o
# vim:ft=make
#

上述$(cc) -c $< -o $@可以自动将每一个.cpp文件编译为相应的.o文件。
可以看到我们定义的depssrc通过shell命令自动搜索路径下的所有文件,obj中的用法表示将src中的所有.c替换为.o作为obj的值

更多用法

多行命令共用shell上下文

默认情况下make执行的每条命令之间是独立的,例如:

1
2
cd /home/blueyi
pwd

这两条命令前后并没有任何关系,为了让pwd与前一条命令共用shell上下文,应该将其让在一行中执行,多条命令通过;(分号)放在一行中。
当命令太长时,可以使用反斜杠(\)续行。

wikipedia上一个通用GNUMakefile

原makefile用于编译C语言,这里改成了C++

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
# Generic GNUMakefile

# Just a snippet to stop executing under other make(1) commands
# that won't understand these lines
ifneq (,)
This makefile requires GNU Make.
endif

PROGRAM = foo
C_FILES := $(wildcard *.cpp)
OBJS := $(patsubst %.cpp, %.o, $(C_FILES))
CC = g++
CFLAGS = -Wall -pedantic
LDFLAGS =
LDLIBS = -lm

all: $(PROGRAM)

$(PROGRAM): .depend $(OBJS)
$(CC) $(CFLAGS) $(OBJS) $(LDFLAGS) -o $(PROGRAM) $(LDLIBS)

depend: .depend

.depend: cmd = g++ -MM -MF depend $(var); cat depend >> .depend;
.depend:
@echo "Generating dependencies..."
@$(foreach var, $(C_FILES), $(cmd))
@rm -f depend

-include .depend

# These are the pattern matching rules. In addition to the automatic
# variables used here, the variable $* that matches whatever % stands for
# can be useful in special cases.
%.o: %.c
$(CC) $(CFLAGS) -c $< -o $@

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

clean:
rm -f .depend $(OBJS)

.PHONY: clean depend

命令前面加个-号(或.IGNORE)

make在执行makefile中的命令时会检测每个命令的返回值,当某个命令出错时(即返回非0),make就会终止当前规则中该命令之后的所有命令,也就有可能终止执行所有规则。有些时候我们的命令返回错误并不影响整个项目的编译,例如mkdirrm等,此时就可以在命令前面加上一个-(减号)表示不管该命令返回值如何,都认为其执行成功,例如上面的clean中的命令可改为-rm -f .depend $(OBJS)
.IGNORE 当使用.IGNORE来做为一个规则(target)的目标时,则表示这个规则中的所有命令执行错误将被忽略。
如果给make加上-i(或--ignore-errors)参数,则该makefile中的所有命令执行错误将被忽略。
如果给make加上-k(或--keep-going)参数,如果某规则中的命令出错,则终止该规则的执行,但会继续执行其他规则。

include引用其他makefile

通过在makefile中使用include<file_name>来引用其他makefile文件,make命令会找到相应的file_name中的内容插入到当前位置,file_name可以含宏以及通配符。include不能以Tab开始。
file_name的查找路径类似C++中的头文件的查找路径,make同样也会去-I(或--include-dir)包含的目录中查找。

函数

函数的用法形如:

1
2
3
$(function arguments)
# OR
${function arguments}

调用方式与变量很像。其中function为函数名,后面为参数,多个参数以,(逗号)分隔,函数名与参数之后用空格分隔。
以替换子字符串的函数subst举例:

1
2
3
4
5
6
comma:= ,
empty:=
space:= $(empty) $(empty) # 通过这种方式定义一个值为空格的变量
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))
# bar is now ‘a,b,c’.

subst的参数作用是第一个参数为将要被替换的子串,第二个参数为替换成的子串,第三个参数为替换操作将发生作用的字符串。
常用函数形如下,(注意:text通常指可以由空白分隔的多个字符串,string指字符串,也可以含空白):

  • $(subst from,to,text) 替换子串函数
  • $(patsubst pattern,replacement,text) 模式匹配替换
  • $(strip string) 去掉string开头和结尾的空白字符
  • $(findstring find,in) 从in中查到find,如果找到,则返回find,否则返回空字符串
  • $(filter pattern…,text) 过滤text中符合pattern的字符串返回
  • $(filter-out pattern…,text) 与上述相反,过滤掉text中符合pattern的字符串,将剩下的返回
  • $(sort list) 将list以字典序排序,并去掉重复的字符串,返回以一个空格分隔的字符串。
  • $(word n,text) 返回text中的第n个字符串,n从1开始
  • $(wordlist s,e,text) 取text中第s个到第e个字符串返回,如:$(wordlist 2, 3, foo bar baz)返回值是“bar baz”
  • $(words text) 返回text中的单词个数
  • $(firstword names…) 返回第一个单词
  • $(lastword names…) 返回最后一个单词

文件名相关的函数

  • $(dir <names...>) 取目录,从文件名序列中取出目录部分,即最后一个反斜杠(/)之前的内容
  • $(notdir <names...>) 取文件名,非目录部分,即文件名,包括扩展名
  • $(suffix <names...>) 取后缀名
  • $(basename <names...>) 取文件名前缀,即不带后缀的文件名
  • $(addsuffix <suffix>,<names...>) 加后缀
  • $(addprefix <prefix>,<names...>) 加前缀,可用于为文件添加路径
  • $(join <list1>,<list2>) 把list2的每个单词对应地添加后list1单词的后面,如:$(join aaa bbb , 111 222 333)返回值是“aaa111 bbb222 333”
  • $(wildcard pattern) 扩展通配符,由于makefile中变量定义和函数引用时,通配符将失效,这咱情况下就可以使用该函数。
  • $(realpath names…) 返回真实绝对地址
  • $(abspath names…) 返回绝对地址,与上一个函数相比,它不会解析链接

foreach函数

大家应该都很熟悉了,就是遍历,形式如下:

1
$(foreach <var>,<list>,<text>)

作用是把参数<list>中的单词逐一取出放到参数<var>所指定的变量中,然后再执行<text>所包含的表达式。每一次<text>会返回一个字符串,循环过程中,<text>的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。
用法举例:

1
2
dirs := a b c d
files := $(foreach dir,$(dirs),$(wildcard $(dir)/*))

类似于files := $(wildcard a/* b/* c/* d/*)
等价于:

1
2
3
find_files = $(wildcard $(dir)/*)
dirs := a b c d
files := $(foreach dir,$(dirs),$(find_files))

shell函数

前面已经用过了,用法如下:

1
contents := $(shell cat foo)


1
`

功能一样。

更多函数参见:https://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions

GNU make的执行步骤

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

VPATH(文件搜索路径)

makefile中可以通过一个特殊的宏名VPATH来指定make命令的文件搜索路径,当不手动定义该宏时,默认情况下make只在当前目录下搜索文件。而有了该宏之后,make会在当前目录搜索不到时去VPATH中搜索。

1
VPATH=src:../header:/usr/include

上述指定了三个搜索目录:src../header/usr/include,make会按照这个顺序去搜索,目录名由冒号分隔。
另一种vpath
采用全小写的vpath关键字可以很灵活地指定不同的文件在不同的目录进行搜索,vpath有3种用法:

  1. vpath <pattern> <directories> 为符合模式<pattern>的文件指定搜索目录<directories>
  2. vpath <pattern> 清除符合模式<pattern>的文件的搜索目录。
  3. vpath 清除所有已被设置好了的文件搜索目录。
    其中<pattern>中需要包含模式匹配符%,如:
    1
    2
    vpath %.h ../header
    vpath %.c build/src

隐藏执行的具体命令

默认情况下make会将需要执行的命令行本身也输出到屏幕上,可以通过在命令行前面加上字符@来让make不显示该命令行本身,而直接执行。
如果make带参数-n(或--just-print),则只是显示命令,而不执行,可以用于调试makefile。
如果make带参数-s(或--slient),则禁止显示所有命令。

嵌套执行子目录中的makefile

当一个工程中包含有多个子目录的工程时,可以通过subsystem来在外层的makefile中嵌套执行各子目录中的makefile,这样就可以为每个子目录编写一个独立的makefile来执行相应目录下的工作。
例如假设我们有一个子目录名为subdir,其目录中有一个makefile,我们的外层目录中的makefile称其为总控makefile,那总控makefile可以如下:

1
2
subsystem:
cd subdir && $(MAKE)

等价于:

1
2
subsystem:
$(MAKE) -C subdir

推荐使用第2种方式,因为该方式将会自动为子make启用-w(或--print-directory)选项,也就是会在执行子make是出现提示:

1
make: Entering directory `/home/src'.

和相应的:

1
make: Leaving directory `/home/src'

其中的$(MAKE)是自己定义宏变量,其中包含make命令和想传递给它的参数。这种情况下子make命令会继承总控make的SHELL变量和MAKEFLAGS变量,但在子makefile中的同名变量会覆盖SHELL中定义的变量。也可以通过export将总控makefile中的宏定义传送到子makefile中。如果子make使用了-e参数,则系统环境变量将覆盖其中的同名变量。
export用法:

1
2
3
export <variable ...>  # 传递相应的宏
unexport <variable ...> #不传递相应的宏
export #传递所有宏

其中

1
2
3
4
export MACRO = value
# 等价于
MACRO = value
export MACRO

内置宏${MAKELEVEL}用于记录嵌套调用的层数,从0开始,第0层也即主控makefile

变量赋值(=, :=, ?=, +=

=
最基本的赋值,使用该赋值时make会将整个makefile展开后进行赋值,也就是说make会取最后的变量值进行赋值。例如:

1
2
3
4
x = foo
y = $(x) bar $(z)
x = abc
z = def

最终y的值是abc bar def,因为make最终确定到的x的值为abc,而不是之前的foo,这样有一个好处是可以引用未被定义的值,上述看到z也被成功展开了。

:=
用于赋值的宏只展开到当前位置,而不去读取后面的赋值。例如:

1
2
3
4
x := foo
y := $(x) bar $(z)
x := abc
z := def

最终y的值为foo bar

?=
作用是如果没有被赋值过就赋予等号后面的值,如果该宏未被定义过,则什么也不做。

+=
用于添加等号后面的值,类似于C++中的+=,如果被赋值的宏未被定义过,则相当于普通等号(=)。赋值方式会继承之前的宏定义时的等号模式(=:=)。

变量支持递推赋值和子串替换

递推赋值

1
2
3
4
5
x = y
y = z
z = hello
a := $($(x))
b := $($($(x)))

注意其结果为:

1
2
a = z
b = hello

子串替换
前面用过了,格式为$(var:a=b)${var:a=b},即将宏var中的所有以a子串“结尾”的a替换为b子串,”结尾”的意思是”空格“或”结束符”。

定义多行变量(define)

当有多个命令需要一起顺序执行时,虽然我们可以使用分号的方式将它们连接在一起,但无法复用这些命令,此时可以通过关键字define定义为多行的变量,然后像引用普通变量一样调用:

1
2
3
4
5
define run_it =
echo $(foo)
echo $(bar)
ls
endef

引用时就直接$(run_it)即可,赋值符号也可以为:=+=,当省略=时,make默认为=号。
注意defineendef之间只能是命令,不能进行变量定义。
可以通过undefine <variable>取消定义的变量。

override取消make命令行的覆盖定义

默认情况下通过make命令行指定的变量定义会覆盖makefile中的定义值,可以通过在变量名前加关键字override来取消make命令行参数对makefile中变量的修改,也就是使用override之后make定义的同名变量将失效。
overridedefine一起使用时,直接将它放在define前面即可。

局部变量

可以为目标定义一个局部变量,这样由该目标所关联的依赖做为目标时的子目标中,该局部变量将都是由目标定义的值,用法如下:

1
2
target … : variable-assignment
target … : override variable-assignment # 针对make传参的形式

variable-assignment可以是上述赋值语句提到的任意一种形式。
例如:

1
2
3
4
5
6
7
8
9
10
11
12
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
$(CC) $(CFLAGS) prog.c

foo.o : foo.c
$(CC) $(CFLAGS) foo.c

bar.o : bar.c
$(CC) $(CFLAGS) bar.c

这样当有全局CFLAGS时并不会影响到目标prog中所使用的CFLAGS值。

模式变量

当目标为符合某一模式时,使用相应的模式下定义的变量,形式:

1
pattern … : variable-assignment

例如:

1
2
3
4
5
6
7
%.o: %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

lib/%.o: CFLAGS := -fPIC -g # 模式变量
%.o: CFLAGS := -g # 模式变量

all: foo.o lib/bar.o

这样就可以针对不同的输出情况调用不同的编译参数了。

条件判断(ifeq..else..endif)

条件判断形式如下:

1
2
3
4
5
6
7
conditional-directive-one
text-if-one-is-true
else conditional-directive-two
text-if-two-is-true
else
text-if-one-and-two-are-false
endif

直接上例子就好:

1
2
3
4
5
6
7
8
9
libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc) # 判断$(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
11
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)

除了ifeq相应的也有以下判断方式:

  • ifneq 判断是否不相等,不相等为true
  • ifdef 判断相应变量的值是否为空,不空为true,注意是指该变量是否被赋过值
  • ifndef 与上面相反

注意:条件判断用于命令部分时,不能以Tab开始

参考

  1. [wikipedia:Make]
  2. GNU make
  3. Makefile简易教程
  4. Makefile经典教程