关于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 | target1 [target ...]: [component1 ...] |
如上,包含了两条规则,通常目标名(即target)要唯一,命令必须以Tab
开始(如果你的vim设置用4个空格替换Tab,可以通过在插入模式下使用ctrl+v+i插入一个Tab键
),依赖文件(即component)将指定命令(command)中需要引用的文件,可以以变量的方式在command中引用传递的依赖文件,以实现复用。当然上述的component部分可以省略,当没有component部分是,称为伪目标,伪目标相当于可以让make帮助我们执行一个shell命令,例如经常会见到的clean
可以看到,一个规则中可以包含多个目标、多个依赖和多条命令。#
开头用于注释
一个简单的makefile,用于编译HelloWorld,为了增加复杂度,将输出函数单独放在一个hello.cpp
和hello.h
的文件中,main函数在main.cpp
中,代码及makefile如下:hello.h
:
1 |
|
hello.cpp
:
1 |
|
main.cpp
:
1 |
|
makefile
:
1 | # |
可以看到后面有一个伪目标clean
用于删除编译生成的文件。
注意:默认情况下make会查看当前目录的目标与依赖相比是否最新,如果是,则不再执行相应命令。这样就会导致如果文件夹下存在了clean文件,而其又没有依赖,那它的命令将永远不会执行。为了解决该问题,可以手动添加一句.PHONY: clean
来指定clean
为伪目标,这样make就总会执行该目标的命令
makefile的规则如此简洁,极易理解和使用,但当我们文件越来越多,并且我们想有更多的控制时,每次都要手动去添加文件就会大大增加工作量,此时可能通过宏来进行更好的实现。
宏(变量)
makefile中可以定义变量,有些地方称之为宏,实际上这里并没有很确切的边界,都是可以的,所以下面会混用这两个概念。定义方式形如:
1 | MACRO = definition |
通过$(MACRO_NAME)
或者${MACRO_NAME}
的方式来引用定义的宏。
通过简单的宏定义,我们可以将上述makefile改写如下,并加入中间文件obj文件的支持:makefile
:
1 | # |
可以看到我们细分了依赖,将头文件作为hello.o和main.o的依赖,而可执行文件则依赖中间文件*.o
可以通过make命令修改makefile中定义的宏:
1 | make MACRO="value" [MACRO="value" ...] TARGET [TARGET ...] |
宏的值可以来源于shell命令,命令由”`”(Esc下面那个键)包围:
1 | TIME = `date` |
多个宏可以直接连接在一起组成一个宏。
举例如下(功能为将当前目录下的文件打包)。makeTar
1 | PACKAGE = package |
执行:
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中的含义完全相同,通配符可以在以下两种场合使用:
- 可以用在规则的目标、依赖中,此时make会自动将其展开;
- 可出现在规则的命令中,其展开是在shell在执行此命令时完成。
利用内置宏和匹配模式,上述HelloWorld的makefile可改写如下:
1 | # |
上述$(cc) -c $< -o $@
可以自动将每一个.cpp
文件编译为相应的.o
文件。
可以看到我们定义的deps
和src
通过shell命令自动搜索路径下的所有文件,obj中的用法表示将src中的所有.c
替换为.o
作为obj的值
更多用法
多行命令共用shell上下文
默认情况下make执行的每条命令之间是独立的,例如:
1 | cd /home/blueyi |
这两条命令前后并没有任何关系,为了让pwd
与前一条命令共用shell上下文,应该将其让在一行中执行,多条命令通过;
(分号)放在一行中。
当命令太长时,可以使用反斜杠(\
)续行。
wikipedia上一个通用GNUMakefile
原makefile用于编译C语言,这里改成了C++
1 | # Generic GNUMakefile |
命令前面加个-
号(或.IGNORE)
make在执行makefile中的命令时会检测每个命令的返回值,当某个命令出错时(即返回非0),make就会终止当前规则中该命令之后的所有命令,也就有可能终止执行所有规则。有些时候我们的命令返回错误并不影响整个项目的编译,例如mkdir
或rm
等,此时就可以在命令前面加上一个-
(减号)表示不管该命令返回值如何,都认为其执行成功,例如上面的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 | $(function arguments) |
调用方式与变量很像。其中function
为函数名,后面为参数,多个参数以,
(逗号)分隔,函数名与参数之后用空格分隔。
以替换子字符串的函数subst
举例:
1 | comma:= , |
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 | dirs := a b c d |
类似于files := $(wildcard a/* b/* c/* d/*)
等价于:
1 | find_files = $(wildcard $(dir)/*) |
shell函数
前面已经用过了,用法如下:
1 | contents := $(shell cat foo) |
与
1 | ` |
功能一样。
更多函数参见:https://www.gnu.org/software/make/manual/make.html#Syntax-of-Functions
GNU make的执行步骤
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
VPATH(文件搜索路径)
makefile中可以通过一个特殊的宏名VPATH
来指定make命令的文件搜索路径,当不手动定义该宏时,默认情况下make只在当前目录下搜索文件。而有了该宏之后,make会在当前目录搜索不到时去VPATH
中搜索。
1 | VPATH=src:../header:/usr/include |
上述指定了三个搜索目录:src
、../header
和/usr/include
,make会按照这个顺序去搜索,目录名由冒号分隔。
另一种vpath
采用全小写的vpath
关键字可以很灵活地指定不同的文件在不同的目录进行搜索,vpath
有3种用法:
vpath <pattern> <directories>
为符合模式<pattern>
的文件指定搜索目录<directories>
。vpath <pattern>
清除符合模式<pattern>
的文件的搜索目录。vpath
清除所有已被设置好了的文件搜索目录。
其中<pattern>
中需要包含模式匹配符%
,如:1
2vpath %.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 | subsystem: |
等价于:
1 | subsystem: |
推荐使用第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 | export <variable ...> # 传递相应的宏 |
其中
1 | export MACRO = value |
内置宏${MAKELEVEL}
用于记录嵌套调用的层数,从0开始,第0层也即主控makefile
变量赋值(=
, :=
, ?=
, +=
)
=
最基本的赋值,使用该赋值时make会将整个makefile展开后进行赋值,也就是说make会取最后的变量值进行赋值。例如:
1 | x = foo |
最终y的值是abc bar def
,因为make最终确定到的x的值为abc
,而不是之前的foo
,这样有一个好处是可以引用未被定义的值,上述看到z也被成功展开了。
:=
用于赋值的宏只展开到当前位置,而不去读取后面的赋值。例如:
1 | x := foo |
最终y的值为foo bar
?=
作用是如果没有被赋值过就赋予等号后面的值,如果该宏未被定义过,则什么也不做。
+=
用于添加等号后面的值,类似于C++中的+=
,如果被赋值的宏未被定义过,则相当于普通等号(=
)。赋值方式会继承之前的宏定义时的等号模式(=
或:=
)。
变量支持递推赋值和子串替换
递推赋值
1 | x = y |
注意其结果为:
1 | a = z |
子串替换
前面用过了,格式为$(var:a=b)
或${var:a=b}
,即将宏var
中的所有以a
子串“结尾”的a
替换为b
子串,”结尾”的意思是”空格“或”结束符”。
定义多行变量(define)
当有多个命令需要一起顺序执行时,虽然我们可以使用分号的方式将它们连接在一起,但无法复用这些命令,此时可以通过关键字define
定义为多行的变量,然后像引用普通变量一样调用:
1 | define run_it = |
引用时就直接$(run_it)
即可,赋值符号也可以为:=
或+=
,当省略=
时,make默认为=
号。
注意define
和endef
之间只能是命令,不能进行变量定义。
可以通过undefine <variable>
取消定义的变量。
override取消make命令行的覆盖定义
默认情况下通过make命令行指定的变量定义会覆盖makefile中的定义值,可以通过在变量名前加关键字override
来取消make命令行参数对makefile中变量的修改,也就是使用override之后make定义的同名变量将失效。
当override
与define
一起使用时,直接将它放在define
前面即可。
局部变量
可以为目标定义一个局部变量,这样由该目标所关联的依赖做为目标时的子目标中,该局部变量将都是由目标定义的值,用法如下:
1 | target … : variable-assignment |
variable-assignment
可以是上述赋值语句提到的任意一种形式。
例如:
1 | prog : CFLAGS = -g |
这样当有全局CFLAGS
时并不会影响到目标prog中所使用的CFLAGS
值。
模式变量
当目标为符合某一模式时,使用相应的模式下定义的变量,形式:
1 | pattern … : variable-assignment |
例如:
1 | %.o: %.c |
这样就可以针对不同的输出情况调用不同的编译参数了。
条件判断(ifeq..else..endif)
条件判断形式如下:
1 | conditional-directive-one |
直接上例子就好:
1 | libs_for_gcc = -lgnu |
等价于:
1 | libs_for_gcc = -lgnu |
除了ifeq
相应的也有以下判断方式:
ifneq
判断是否不相等,不相等为trueifdef
判断相应变量的值是否为空,不空为true,注意是指该变量是否被赋过值ifndef
与上面相反
注意:条件判断用于命令部分时,不能以Tab
开始
参考
- [wikipedia:Make]
- GNU make
- Makefile简易教程
- Makefile经典教程