文章目录
- 一、简单了解Makefile
- 1.1 Makefile示例
- 1.2 基本规则
- 1.3 make是如何工作的
- 1.4 使用变量
- 1.5 make自动推导
- 二、变量
- 2.1 变量的定义和引用
- 2.2 变量的两种高级用法
- 2.3 override 和 define 关键字
- 2.4 环境变量与目标变量
- 2.5 自动变量
- 三、Makefile规则
- 3.1 通配符
- 3.2 目标依赖
- 四、条件判断
- 4.1 ifeq、ifneq 判断条件是否相等
- 4.2 ifdef、ifndef 判断变量值是否为空
- 五、函数
- 5.1 字符串处理函数
- 5.2 文件名处理函数
- 5.3 foreach 函数
- 5.4 if 函数
- 5.5 call 函数
- 5.6 origin 函数
- 5.7 shell 函数
- 5.8 error 和 warning 函数
一、简单了解Makefile
1.1 Makefile示例
使用Makefile编写规则编写一个输出Hello world的程序,程序文件如下:
$ cat print.h
#include<stdio.h>
void printhello();$ cat print.c
#include"print.h"
void printhello(){printf("Hello, world\n");
}$ cat main.c
#include "print.h"
int main(void){printhello();return 0;
}
编写的Makefile文件如下:
helloworld : main.o print.occ -o helloworld main.o print.o
mian.o : mian.c print.hcc -c main.c
print.o : print.c print.hcc -c print.cclean :rm helloworld main.o print.o
1.2 基本规则
make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。以下是Makefile最基本的规则:
target ... : prerequisites ...command......
target
就是一个目标,可以是Object File,也可以是执行文件,还可以是一个标签(Label)。prerequisites
就是要生成那个target
所需要的文件或是target
。如果是target
,则该target
在后面会有一个对应的规则(eg:print.o)。command
就是make需要执行的命令。command
前必须使用[Tab]键
,使用空格会报错。
make会比较targets
文件和prerequisites
文件的修改日期,如果prerequisites
文件的日期要比targets
文件的日期要新,或者target
不存在的话,那么,make
就会执行后续定义的命令。
1.3 make是如何工作的
下文所说的helloworld目标,指的是Makefile中的target,helloworld文件 指的是项目中名为helloworld的文件,注意区分。在默认的方式下,也就是我们只输入make命令,那么:
- make会在当前目录下找名字叫 Makefile 或 makefile 的文件。
- 如果找到,它会找文件中的第一个目标target。如上例中,会找到helloworld目标,并将其作为最终目标文件的文件名。然后从helloworld目标开始依次寻找依赖关系。
- 如果helloworld文件存在(条件1),且helloworld目标文件比其所依赖的
.o
目标文件的文件修改时间新(条件2),且.o
目标文件比其所依赖的.c
和.h
文件的文件修改时间新(条件3),则make啥也不做 - 条件1不成立,条件2和3成立,则只会执行helloworld目标定义的命令。
- 条件2不成立,条件3成立,则不管条件1是否成立,只会执行helloworld目标定义的命令。
- 如果条件3不成立,不管条件1和2是否成立,helloworld目标以及
.o
目标中定义的命令,都会执行。.o
目标有两个,哪个不满足条件就执行哪个目标定义的命令,满足的那个不执行。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
1.4 使用变量
在Makefile示例中,main.o
和print.o
在prerequisites和command总共出现了3次,如果再有新的xxx.o
规则,在这三个地方都要加。所以,为了makefile的易维护,在makefile中我们可以使用变量。如下就是对变量OBJECTS
的定义和使用:
OBJECTS = main.o print.ohelloworld: $(OBJECTS)cc -o helloworld $(OBJECTS)
mian.o: mian.c print.hcc -c main.c
print.o: print.c print.hcc -c print.cclean:rm helloworld $(OBJECTS)
1.5 make自动推导
GNU的make很强大,它具有一些隐晦规则,可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个.o
文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个.o
文件,它就会自动的把.c
和.h
文件加在依赖关系中,如make找到一个print.o
,那么print.c
和print.h
,就会是print.o
的依赖文件,并且 cc -c print.c
也会被推导出来。
OBJECTS = main.o print.ohelloworld: $(OBJECTS)cc -o helloworld $(OBJECTS)
mian.o:
print.o:clean:rm helloworld $(OBJECTS)
甚至还可以按照下面方式写,不过这样Makefile文件依赖关系就显得有点凌乱了,可以根据实际情况选择,别人写了要能看懂。
OBJECTS = main.o print.ohelloworld: $(OBJECTS)cc -o helloworld $(OBJECTS)
$(OBJECTS):clean:rm helloworld $(OBJECTS)
二、变量
在 Makefile 中的定义的变量,就像是 C/C++语言中的宏一样,他代表了一个文本字符串,在 Makefile 中执行的时候其会自动原模原样地展开在所使用的地方。其与 C/C++所不同的是,你可以在 Makefile 中改变其值。
变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有:
、#
、 =
或是空字符
(空格、回车等)。变量是大小写敏感的, foo
、 Foo
和FOO
是三个不同的变量名。
2.1 变量的定义和引用
定义或改变一个变量常用以下4种等号:
:=
:立即变量。对于右边引用的变量,在定义左边变量时就会展开,且只有到目前为止定义过的变量才会得到展开。=
:延迟变量。对于右边引用的变量,在执行含有左边变量的命令时才会展开,而不是在定义左边变量时展开。?=
:如果左边变量之前没有被定义过,那么变量的值就是右边值,如果变量先前被定义过,那么这条语将什么也不做。+=
:将等号右边的值追加到左边变量中。如果变量之前没有定义过,那么,+=
会自动变成=
;如果前面有变量定义,那么+=
会继承于前次操作的赋值符;如果前一次的是:=
,那么+=
会以:=
作为其赋值符。
变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及“变量的值”中。引用一个变量可使用$()
、${}
这两个符号,使用第一个居多。
one = hello
#不允许将变量自己的值赋给自己,因为one会递归引用自身
#one = ${one} world
#这样写就允许,不会递归引用
one := ${one} world #这里加注释后,one的值为"hello world ",后面多一个空格all:echo $(one)
2.2 变量的两种高级用法
第一种是变量值的替换,其格式为$(var:a=b)
或是${var:a=b}
,意思是,把变量var
中所有以a
子串结尾
的a
替换成b
子串。这里的结尾
意思是空格
或是结束符
。
OBJECTS = main.o print.o
SOURCE = $(OBJECTS:.o=.c)
all:echo $(SOURCE)
第二种是把变量的值再当成变量,其格式为$($(var))
,意思是,把变量var的值作为变量名并对其引用。
x = y
y = z
z = value
a := $($($(x)))
2.3 override 和 define 关键字
make是可以通过命令行设置变量和值的,使用override
定义一个变量,则通过命令行对这个变量的赋值会被忽略。
# 执行make one=boy会输出boy
one = hello
all:echo $(one)# 执行make one=boy会输出hello,override会忽略命令行对one的赋值
override one = hello
all:echo $(one)
define
后面跟的是变量的名字,而重起一行定义变量的值或执行命令,定义是以endef
关键字结束。
define
实际上只是一个命令列表,这与命令之间的分号有所不同,因为列表的每个命令都在单独的shell中运行。Linux命令行在shell脚本和Makefile会有些不一样的差别,一个shell是一个进程,shell脚本的命令都是在shell一个进程进行,前后命令会有所影响;而makefile里的每一行命令是一个单独的进程,只在单行里有影响,不对上下文影响。
one = export blah="I was set!"; echo $$blahdefine two
export blah=set
echo $$blah
endefall: @echo "这会打印 'I was set'"@$(one)@echo "这不会打印 'I was set' 因为每个command都在单独的shell中运行"@$(two)
2.4 环境变量与目标变量
除了用户自定义的一些变量,make在解析Makefile中还会引入一些系统环境变量,如编译参数CFLAGS、SHELL、MAKE等。这些变量在make开始运行时被载入到Makefile文件中,因为是全局性的系统环境变量,所以这些变量对所有的Makefile都有效。若Makefile中有用户自定义的同名变量,系统环境变量将会被用户自定义的变量覆盖。若用户在命令行中传递跟系统环境变量同名的变量,系统环境变量也会被传递的同名变量覆盖。(如果make指定了-e
参数,那么,系统环境变量将覆盖Makefile中定义的变量)
one = hello
all:@echo $(one)$ export one=boy
$ make
hello
$ make -e
boy
为特定的目标定义变量,该变量的作用域只在特定目标下,且在作用域内是被最先匹配的,即先于文件变量和环境变量(前提make执行时没加-e
参数)。
one = boy
all: one = coolall: # 输出coll@echo $(one)other: # 输出boy@echo $(one)
2.5 自动变量
自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:
-
:告诉make在编译时忽略所有的错误@
:告诉make在执行命令前不要显示命令$@
:所有目标文件$^
:所有目标依赖$<
:目标依赖列表中的第一个依赖$?
:所有目标依赖中被修改过的文件$%
:当规则的目标是一个静态库文件时,$%
代表静态库的一个成员名$+
:类似$^
,但是保留了依赖文件中重复出现的文件$*
:在模式匹配和静态模式规则中,代表目标模式中%
的部分。比如hello.c
,当匹配模式为%.c
时,$*
表示hello
$(@D)
:表示目标文件的目录部分$(@F)
:表示目标文件的文件名部分$(*D)
:在模式匹配中,表示目标模式中%的目录部分$(*F)
:在模式匹配中,表示目标模式中%的文件名部分
三、Makefile规则
3.1 通配符
在Makefile中,常用的通配符是*
和%
。两者的共同点是都代表任意长度的字符;两者的区别在于,*
是应用在当前目录中来匹配文件或目录,%
是应用在当前文件中来匹配Makefile相应规则。两者的应用场合为:
*
主要应用在规则的依赖中、规则的命令中、以及变量的值中。除了命令中,在其他地方都不建议直接使用通配符,而是用一些函数,如想列举当前目录下的所有C文件,可用$(wildcard *.c)
。%
主要应用在规则的目标中、规则的依赖中、以及一些函数中(字符串查找替换等)。
用在规则的目标和依赖中,make在读取Makefile时会自动对其进行匹配处理(通配符展开)。用在规则的命令中,通配符的通配处理在shell执行命令时完成。
# 找到当前目录下所有以.c为后缀的文件,将后缀.c替换成.o,如:main.c => main.o,然后作为依赖
all: $(subst .c,.o,$(wildcard *.c))
# 上面的依赖会在这里匹配,如main.o与%.o匹配,所以%.c就成了main.c,相当于main.o: main.c
%.o: %.cgcc -c $<
# 删除当前目录下所有以.o结尾的文件
clean:rm -f *.o
3.2 目标依赖
默认目标:一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。
多目标:一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。如一个目标文件%.o
都是由其对应的源文件%.c
编译生成的,生成命令也是相同的。
%.o: %.cgcc -o %.o %.c
多规则目标:多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。如果每个相同目标后跟一个冒号:
,则多个目标只能有一个目标有执行命令,否则会报错;如果每个相同目标后跟双冒号::
,则多个目标能有多个执行命令。
# 单冒号,只能一个目标有命令
helloworld: main.occ -o helloworld $(OBJECTS)
helloworld: print.o# 双冒号,多个目标可以有命令
blah::@echo "hello"
blah::@echo "hello again"
伪目标:使用.PHONY
表示目标是一个伪目标。伪目标一般没有依赖关系,也不会生成对应的目标文件,可以无条件执行,纯粹是为了执行某一个命令,如clean执行清理工作。
.PHONY : clean
clean:-rm helloworld $(OBJECTS)
头文件依赖
make会根据时间戳来判断一个规则中的目标依赖文件是否有更新。make在编译程序时,会依次检查依赖关系树中的所有源文件的时间戳,如果发现某个文件的时间戳有更新,会认为这个文件有改动过,会重新编译这个源文件。如果发现文件的时间戳没有更新,就不会再重新编译一次。
在Makefile的规则中,一般不会把头文件添加到目标依赖中。当一个.c
文件中包含多个头文件时,如果对应的头文件发生了变化,因为头文件没有包含在依赖关系树中,所以这个.c
文件就不会重新编译。如我们的 1.5 的Makefile,修改print.h
文件,并不会重新helloworld。有两种方式解决这个问题:
- 手动将头文件添加到规则中,一般不采取。
- 一个更高效的解决方法是:使用
gcc -M
命令自动生成头文件依赖关系。
四、条件判断
4.1 ifeq、ifneq 判断条件是否相等
ifeq
关键字用来判断两个参数是够相等,相等时条件成立为true,不相等为false。ifeq
一般和变量结合使用:
mode = debugall:
ifeq ($(mode),debug)@echo "debug mode"
else@echo "release mode"
endif
ifneq
关键字和ifeq
关键字恰恰相反,用来判断参数是否不相等。当比较的参数不相等时,条件语句才成立,值为true,否则为false。
4.2 ifdef、ifndef 判断变量值是否为空
ifdef
关键字用来判断一个变量是否已经定义,如果变量的值非空(在Makefile中,没有定义的变量的值为空),表达式为true。
mode = debugall:
ifdef mode@echo "def mode"
else@echo "ndef mode"
endif
ifndef
关键字和ifdef
相反,如果一个变量没有定义,表达式为true。ifdef
和ifndef
后面直接跟变量名,不用引用符号。
五、函数
关于函数的使用格式,有以下需要注意的地方:
- 函数主要分为两类:make内嵌函数和用户自定义函数。对于 GNU make内嵌的函数,直接引用就可以了;对于用户自定义的函数,要通过make的
call
函数来间接调用。 - 函数和参数列表之间要用空格隔开,多个参数之间使用逗号隔开(没有空格)。
- 如果在参数中引用了变量,变量的引用建议和函数引用使用统一格式:要么是一对小括号,要么是一对大括号。
make内嵌的函数调用语法如下:
$(<function> <arg1>,<arg2>,...)
# 或者
${<function> <arg1>,<arg2>,...}
5.1 字符串处理函数
GNU make提供了一系列文本处理函数:
$(subst old,new,text)
subst函数用来实现字符串的替换,将字符串text中的old替换为new$(patsubst pattern,replacement,text)
patsubst函数用来做模式替换:查找text中的单词(单词以"空格",“tab”,"换行"来分割)是否符合pattern,符合的话,用replacement替代。$(strip text)
strip函数用来将多个连续的空字符合并成一个,包括字符串开头、末尾的空字符。空字符包括:空格、多个空格、tab等不可显示的字符。$(findstring find,text)
findstring函数会在字符串text中查找"find"字符串,如果找到,则返回字符串find,否则,返回空。$(filter pattern…,text)
filter函数用来过滤掉字符串text中所有不符合pattern模式的单词,只留下符合pattern格式的单词。$(filer-out pattern…,text)
filer-out函数是一个反过滤函数,功能和filter函数恰恰相反:该函数会过滤掉所有符合pattern模式的单词,保留所有不符合此模式的单词$(sort text)
sort函数对字符串LIST中的单词以首字母为准进行排序,并删除重复的单词。$(word n,text)
word函数从字符串text中,取出第n个单词。n大于字符串中单词的个数,返回空;如果n为0,则出错。$(wordlist n,m,text)
wordlist函数用来从一个字符串text中取出第[n,m]个单词之间的一个单词串,n和m都是从1开始的一个数字。$(words text)
words函数用来统计一个字符串text中单词的个数。$(firstword text)
firstword函数用来取一个字符串中的首个单词,相当于$(word 1,text)
STR = a.c b.h c.s d.cpp
.PHONY: all
all:@echo $(subst not,totally,I am not superman)@echo $(patsubst %.c,%.o,$(wildcard *.c))@echo $(strip hello world )@echo $(findstring hello,hello world)@echo $(filter %.c,$(STR))@echo $(filter-out %.c,$(STR))@echo $(sort $(STR))@echo $(word 2,$(STR))@echo $(wordlist 2,4,$(STR))@echo $(wordlist 2,4,$(STR))@echo $(words $(STR))@echo $(firstword $(STR))
5.2 文件名处理函数
GNU make提供了一系列对文件名进行各种操作的函数:文件名替换、加前缀、去目录等。
$(dir NAMES…)
dir函数:取路径名的目录。dir函数会从NAMES文件名序列中,取出各个文件路径名中的目录部分并返回。$(notdir NAMES…)
notdir函数:取文件名。$(suffix NAMES…)
suffix函数:取文件名后缀。文件名的后缀是文件名中以点号.
开始(包括点号)的部分。若文件名没有后缀,suffix函数则返回空。$(basename NAMES…)
basename函数:取文件名前缀。$(addsuffix SUFFIX,NAMES…)
addsuffix函数:给文件名加后缀。给文件列表中的每个文件名添加后缀SUFFIX。$(addprefix PREFIX,NAMES…)
addprefix函数:给文件名加前缀$(join LIST1,LIST2)
join函数的作用是:将字符串LIST1和字符串LIST2的各个单词依次连接,合并为新的单词构成的字符串。这是将字符串中每个对应位置上的单词连接,而不是连接字符串。$(wildcard PATTERN)
wildcard函数的作用是:列出当前目录下所有符合PATTREN模式的文件名。
FILE_PATH := /home/loongson/workspace/makefile-test/main.c
FILE_PATH += $(FILE_PATH).PHONY: all
all:@echo $(dir $(FILE_PATH))@echo $(notdir $(FILE_PATH))@echo $(suffix $(FILE_PATH))@echo $(basename $(notdir $(FILE_PATH)))@echo $(addsuffix .o,$(basename $(notdir $(FILE_PATH))))@echo $(addprefix test,$(suffix $(notdir $(FILE_PATH))))@echo $(join $(basename $(notdir $(FILE_PATH))),$(suffix $(FILE_PATH)))@echo $(wildcard *.c)
5.3 foreach 函数
如果想做一些循环或遍历操作时,可以使用foreach
函数:
$(foreach var,list,test)
foreach
函数的工作过程是:把list
中使用空格分割的单词依次取出并赋值给变量var
,然后执行text
表达式。重复这个过程,直到遍历完list
中的最后一个单词。函数的返回值是text
多次计算的结果。
# 找出dirs所有目录下的所有.c文件
.PHONY: all
dirs = hello-demo test
srcs = $(foreach dir, $(dirs), $(wildcard $(dir)/*.c))
all:@echo $(srcs)
5.4 if 函数
if
函数提供了在一个函数上下文中实现条件判断的功能,类似于ifeq
关键字,if
函数的使用格式如下:
$(if CONDITION,THEN-PART)
$(if CONDITION,THEN-PART[,ELSE-PART])
if
函数的第一个参数 CONDITION
表示条件判断,展开后如果非空,则条件为真,执行 THEN-PART
部分;否则,如果有ELSE-PART
部分,则执行ELSE-PART
部分。
if
函数的返回值即执行分支(THEN-PART
或ELSE-PART
)的表达式值。如果没有ELSE-PART
,则返回一个空字符串。
# 指定安装路径,默认则是/usr/local
.PHONY: all
install_path =
all:@echo $(if $(install_path),$(install_path),/usr/local)
5.5 call 函数
用户自定义函以define
开头,endef
结束,给函数传递的参数在函数中使用$(0)
、$(1)
引用,分别表示第1个参数、第2个参数…
使用call
函数可以用来间接调用用户自定义函数,各个参数之间使用空格
隔开:
.PHONY: all
define func@echo "pram1 = $(0)"@echo "pram2 = $(1)"
endef
all:$(call func, hello zhaixue.cc)
call
函数不仅可以用来调用一个用户自定义函数并传参,还可以向一个表达式传参:$(call <expression>,<parm1>,<parm2>,<parm3>...)
.PHONY: all
param = $(1) $(2)
str1 = $(call param, hello, zhaixue.cc)
all:@echo $(str1)
5.6 origin 函数
origin
函数的使用格式为:$(origin <variable>)
如果变量没有定义,origin
函数的返回值为:undefined
,不同的返回值代表变量的类型不同。常见的返回值如下:
default
:变量是一个默认的定义,比如 CC 这个变量。file
:这个变量被定义在Makefile中。command line
:这个变量是被命令行定义的。override
:这个变量是被override指示符重新定义过的。automatic
:一个命令运行中的自动化变量。
.PHONY: all
WEB = www.zhaixue.cc
web_type = $(origin WEB)
all:@echo $(origin WEB)@echo $(origin CC)@echo $(origin CMD)# make
# make CMD=pwd
5.7 shell 函数
如果你想在Makefile中运行shell
命令,可以使用shell
函数来完成这个功能。shell
函数的参数是shell
命令,它和反引号
具有相同的功能。shell
命令的运行结果即为shell
函数的返回值。
.PHONY: all
all:@echo $(shell pwd)@echo $(shell ls -m)
5.8 error 和 warning 函数
make提供了两个可以控制make运行方式的函数:error
和warning
。两个函数都会产生错误提示信息,但是error
会终止make的运行,而warning
则不会。
.PHONY: all
all:@echo "make command start..."$(warning find a error)#只发出提示信息$(error find a error)#发出提示信息,并终止make运行@echo "make command end..."