在Window操作系统中,我们可以使用很多优秀的IDE(Integrated Development Environment)来进行开发,如Visual Studio等,在这些集成开发环境中,我们只需要编写好代码再点击编译就可以运行了。而在Linux操作系统中,通常我们编写好源码后需要使用GCC等编译器进行手动编译。当我们的项目文件有大量的源文件(.c)及头文件(.h)时,手动进行编译就会非常繁琐,这时候如果一个工具,我们只需要提供输入输出及依赖就可以帮助我们完成整个编译过程,那么将会大大减少我们的工作量,提高我们的开发效率。
Makefile的作用就是帮助我们解决上面这个问题,我们只需要编辑好整个项目的Makefile文件后通过一个make命令就能够实现自动编译。准确地说,make是一个命令工具,是一个解释Makefile中指令的命令工具,它会根据我们编写的Makefile来确定哪些文件需要进行编译。
以C语言最经典的入门程序Hello World为例,在Linux下编译好该源代码后,我们通常是通过gcc -c helloworld.c -o helloworld.o
以及gcc helloworld.o -o helloworld
命令进行编译来生成可执行文件的文件。我们将上述命令改造为Makefile文件,通过make命令来进行自动编译。
helloworld.c
文件:
#include
int main()
{
printf("Hello World");
return 0;
}
Makefile
文件:
写法一:
helloworld : helloworld.o
gcc helloworld.o -o helloworld
helloworld.o : helloworld.c
gcc -c helloworld.c -o helloworld.o
写法二:
helloworld : helloworld.c
gcc -c helloworld.c -o helloworld.o
gcc helloworld.o -o helloworld
在上述编写的写法一Makefile文件中,根据我们编译helloworld.c的过程,首先我们最后生成的可执行文件helloworld需要helloworld.o目标文件,而生成可执行文件helloworld则是通过命令gcc helloworld.o -o helloworld
;接着我们生成helloworld.o目标文件需要helloworld.c源文件,通过命令gcc -c helloworld.c -o helloworld.o
来生成。这里我们简单了解Makefile的基本规则,根据我们最终生成的文件反推回去所需要依赖的文件来编写,并按照 目标文件 : 依赖文件 的格式来编写,接着在下方编写生成目标文件的指令(注意指令前面要按TAB键)。当然我们也可以按照写法二来进行Makefile文件的编写,此时需要注意依赖文件的先后顺序,需要先执行gcc 命令生成目标文件helloworld.o再执行gcc命令生成可执行文件hellowolrd。
在上述例子中我们只是编译了单个源文件,还不能够体现出编写Makefile文件进行自动编译的便捷性。这里我们编写两个源文件(.c)来实现一个加法功能,通过手动编译以及编写Makefile文件进行自动编译来体会两者的效率。
add.c
文件:
int add(int num_a, int num_b)
{
return num_a + num_b;
}
main.c
文件:
#include
int add(int num_a,int num_b);
int main()
{
int rst;
rst = add(1,2);
printf("1 + 2 = %d\n",rst);
return 0;
}
编写完成上述两个源文件后,我们通过命令gcc -c add.c -o add.o
生成目标文件add.o、gcc -c main.c -o main.o
生成目标文件main.o,最后通过命令gcc main.o add.o -o main
将两个目标文件(add.o和main.o)链接生成可执行文件main。这时候如果对上面两个源文件进行了改动,我们都需要重新编译生成目标文件再将两个目标文件链接生成可执行文件,这将会给我们的开发增加难度,大大降低我们的开发效率。我们编写Makefile文件来实现上述的编译过程,这时候只需要通过一个简单的指令make
就可以替我们实现上面的整个过程,并且它还会智能检测我们哪一些文件是需要生成的,哪一些文件是不需要的。
main : main.o add.o
gcc main.o add.o -o main
main.o : main.c
gcc -c main.c -o main.o
add.o : add.c
gcc -c add.c -o add.o
此时,我们修改main.c源文件,将1 + 2修改为1 + 3,直接执行命令make
进行重新编译后运行可执行文件main,得到1 + 3的结果。
main.c
文件:
#include
int add(int num_a,int num_b);
int main()
{
int rst;
rst = add(1,3);
printf("1 + 3 = %d\n",rst);
return 0;
}
执行结果:
$ ./main
1 + 3 = 4
在Makefile文件中定义的变量,与在C语言中定义的宏类似代表一个字符串文本,在执行Makefile的时候会用该变量的值进行替换,使用变量可以使得Makefile文件易于维护和扩展。
变量的命名可以是字母、数字、下划线的任意组合形式,但不能够包含:
、#
、=
或
(空格)。变量名是大小写敏感的,当需要使用变量时需要在变量名前加上$
符号,并且建议在使用中用圆括号()
或花括号{}
将变量名包括起来,如果需要使用真实字符$
,则需要用$$
来表示,以下为变量的三种使用方式(常用方式二):
variable = test
$variable
$(variable)
${varialbe}
Makefile中的变量只能是字符串类型,它包括四种赋值方式:
简单赋值(:=
) 如果赋的值存在变量,那么该变量的值为执行此赋值语句前的值,否则直接赋值。
a := 1
b := $(a)2
a := 3
此时a和b的值分别为 3 以及 12,相当于C语言中的变量赋值,将变量a的值拼接2后的结果赋予b。
递归赋值(=
) 如果赋的值存在变量,那么会以该变量最终赋的值来进行替换,否则直接赋值。
a = 1
b = $(a)2
a = 3
此时a和b的值分别为 3 以及 32,与简单赋值不同,使用递归赋值如果存在变量会以该变量的最终结果来赋值,所以可以使用赋值语句后面定义的变量。
条件赋值(?=
) 如果赋值的变量在此之前未进行定义,那么会对变量进行赋值;若已经定义,则该赋值语句无效。
a := 1
b := $(a)2
a ?= new
此时a和b的值分别为 1 以及 12,在执行a ?= new
时,由于之前已经执行了a := 1
进行赋值,所以这里不会进行赋值。
追加赋值(+=
) 将在赋值的变量后面追加新的值,原变量值与新追加的值之间由空格隔开。
a := 1
b := $(a)2
a += b
此时a和b的值分别为 1 12 以及 12,执行a += b
进行赋值时,原先a的值为1,b的值为12,将ab进行拼接并在中间增加一个空格。
make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但如果Makefile文件中已经定义了这个变量,或是这个变量由make命令行指定,那么将会覆盖系统的环境变量的值。(如果make命令指定了-e
参数,那么系统环境变量会覆盖Makefile中定义的变量。)
在3.1.1中我们自定义的变量的作用域为整个Makefile文件,即相当于C语言中的全局变量。同样地,我们也可以为某个目标设定局部变量,这种变量被称为"Target-specific Variable"目标变量,因为它的作用范围只在某条规则以及其连带规则中,所以它可以和"全局变量"同名,其语法结构为:
: [=、:=、?=、+=、]
这个特性非常有用,当我们设置一个这样的局部变量后,可以在指定的目标中及其所包含的所有规则中生效。如在test中改变a的值并打印出来:
a := 1
b := $(a)2
test : a = 3
test :
@echo "a => $(a)"
@echo "b => $(b)"
在上述的Makefile文件中,当执行make test
命令时,由于在test中对a执行了赋值a = 3
而对b进行b := $(a)2
赋值时a的值为1,所以输出的a和b分别为 3 和 12。
在make中还支持模式变量(Pattern-specific Variable),它是目标变量的扩展,可以根据相应的规则作用到符合规则的目标中。在模式变量中必须包含一个"%
“字符,用来匹配任意字符,例如想匹配所有以**.o**结尾的字符串可以使用”%.o
"来匹配,其语法结构与目标变量一致:
: [=、:=、?=、+=、]
在编写Makefile文件时使用模式变量能够大大简化我们编写的文件,提高Makefile文件的可读性以及扩展性。如在所有以 t
结尾的目标中将变量a重新赋值为new value:
a := 1
b := 2
%t : a = new value
test :
@echo "a => $(a)"
@echo "b => $(b)"
此时执行命令 make test
后,由于模式变量%t匹配到了目标test所以会对a赋值a = new value,打印的a和b的值分别为 new value 和 2。
当我们make执行Makefile文件时,如果想在使用make
命令时修改Makefile中定义的变量的值,那么只需要在执行make
命令时直接输入
即可进行赋值。当我们不想Makefile文件中的变量被随意改变时,只需要对该变量使用override
进行修饰,就会忽略该变量在make
命令行参数中的赋值,其语法结构为:
override [=、:=、?=、+=、]
例如通过在make
命令行参数中动态指定欲生成的可执行文件名,若不指定则以默认文件名(main)来生成:
filename = main
$(filename) : main.o add.o
gcc main.o add.o -o $(filename)
main.o : main.c
gcc -c main.c -o main.o
add.o : add.c
gcc -c add.c -o add.o
当执行命令make filename=run
时就会生成可执行文件run,若想禁止通过命令行参数进行修改,只需要在filename
前加上override
即可。
在Makefile中还可以通过使用define
关键字来修饰一个变量,被修饰的变量在赋值的时候可以进行换行,其变量的值可以是函数、命令、文字或是其他变量,命令需要以Tab键开头,其语法结构为:
define
·
·
·
·
·
·
endef
例如通过定义一个多行变量并调用该变量来实现多个变量的打印输出:
a = 1
b = 2
define show
@echo "a => $(a)"
@echo "b => $(b)"
endef
test :
$(show)
当执行命令make test
后打印输出 a => 1 和 b => 2。
可以将变量中共有的部分进行替换,如将某个变量的值中所有以a结尾的字符串中的最后一个字符a替换为b,这里的结尾是指字符串中的空格或结束符,其语法结构为:
[=、:=、?=、+=、] $(:=)
[=、:=、?=、+=、] ${:=}
例如将所有以.o结尾的字符串替换成以.c结尾 :
old = a.o b.o c.o
new = $(old:.o=.c)
test :
@echo $(new)
当执行命令make test
后打印输出变量new的值为 a.c b.c c.c。
我们在修改某个变量值时很多时候并不一定只是它的一部分,这时候上面的方法就不能够满足我们的需求了,那么我们可以通过在表达式中使用通配符%
来进行匹配,其语法结构与上述一致。例如对某个变量的值,其形式都是以a开头和以b结尾的,我们需要将开头和结尾替换成x和y,可以通过编写以下Makefile文件来实现这个效果:
old = a123b a456b a789b
new = ${old:a%b=x%y}
test :
@echo $(new)
当执行命令make test
后打印输出变量new的值为 x123y x456y x789y。在日常开发过程中,通常会使用这种方式来进行变量替换引用的操作。
我们对一个变量进行赋值时可以引用其他的变量,并且引用变量的数量和次数是不限的。如对一个变量赋值时进行两层的嵌套引用:
x = y
y = double reference
z = $($(x))
test :
@echo $(z)
当执行命令make test
后打印变量z的值为 double reference。首先对 z 进行赋值时进行了变量的嵌套引用,最里层是$(x)
引用了变量x的值为 y,将$(x)
替换为 y 再进行赋值,此时赋值语句变为z = $(y)
,最后将 y 的值 double reference 赋给变量 z 。
在Makefile文件中,我们可以使用条件判断让make根据运行时不同的情况来选择不同的执行分支,其语法结构如下:
endif
或
else
endif
其中
表示条件关键字,一共有四种条件关键字。
ifeq
关键字
比较参数arg1和arg2的值是否相同,可以为以下五种语法中的任意一种:
ifeq (,)
ifeq ''''
ifeq """"
ifeq ""''
ifeq ''""
ifneq
关键字
比较参数arg1和arg2的值是否不相同,与ifeq
关键字的语法相同。
ifdef
关键字
判断变量的值是否非空,非空则为真,否则为假,其语法结构为:
ifdef
ifndef
关键字
判断变量的值是否为空,空则为真,否则为假,与ifdef
关键字的语法相同。
判断一个变量的值是否为空:
x =
y = $(x)
ifDef :
ifdef y
@echo not empty
else
@echo empty
endif
ifEq :
ifeq ($(y),)
@echo empty
else
@echo not empty
endif
test : ifDef ifEq
当执行命令make test
后打印值为 not empty 和 empty 。通过上面的例子可以发现,当我们要判断一个变量的值是否为空时应该使用ifeq
关键字来判断,而对于ifdef
关键字我的理解是判断一个变量是否有定义;在定义变量y时引用了变量x的值,而变量x赋值为空,所以变量y的值也为空。
自动化变量可以理解为由Makefile自动产生的变量。在Makefile中描述规则时,依赖文件和目标文件是变动的,这时候我们不应当使用具体的文件名称,那么如何书写一个命令来完成从不同的依赖文件生成对应的目标?这时候我们就应该使用Makefile提供的自动化变量。使用Makefile提供的自动化变量,我们不需要关心这些不确定的目标和依赖,只需要编写其生成过程的命令,让Makefile自动地帮我们获取相对应的值并替换进去即可。
变量 | 含义 |
---|---|
$@ |
表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@ 就是匹配于目标中模式定义的集合。 |
$% |
仅当目标是函数库文件中,表示规则中的目标成员名。 |
$< |
依赖目标中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么$< 将是符合模式的一系列的文件集。 |
$? |
所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件是静态库文件,代表的是库文件(.o 文件)。 |
$^ |
所有的依赖目标的集合,以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。 |
$+ |
与$^ 变量类似,也是所有依赖目标的集合,但当依赖目标存在重复的时候它不会去除重复的依赖目标。 |
$* |
这个变量表示目标模式中"%"及其之前的部分,在日常使用中应尽量避免使用该变量,除非是在隐含规则或是静态模式中。 |
使用上面的这些自动化变量,我们可以写一个Makefile文件来快速编译一个有多个源文件的项目:
NAME ?= main
OBJS := main.o add.o
CC := gcc
$(NAME) : $(OBJS)
$(CC) $^ -o $@
%.o : %.c
$(CC) -c $< -o $@
在这个Makefile文件中,我们将默认定义生成的可执行文件名为main,除非我们手动在make
命令行参数中指定输出名。同样地,我们定义了一个OBJS变量来存放我们所依赖的文件,以及CC变量存放我们的编译命令,提高这个Makefile文件的扩展性。
在Makefile文件中除了可以定义变量,同样地我们也可以使用函数来帮助我们实现各种功能,从而使Makefile文件更加灵活和智能。函数的调用与变量的使用方式相似,也需要使用$
来标识,其语法结构如下:
$( )
${ }
subst
函数
字符串中的
字符串替换为
字符串。$(subst ,,)
patsubst
函数
字符串中的子字符串(以空格、Tab、回车、换行分隔)是否符合
模式,若匹配则以
进行替换。其中
可以是包含通配符%
的任意长度字符串,若
中也包含通配符%
,则
中通配符%
的值为
通配符%
所匹配的值,如果需要使用真实字符%
,需要使用\%
来表示。$(patsubst ,,)
strip
函数
字符串中开头和结尾的空字符。$(strip )
findstring
函数
字符串中查找
字符串。
字符串;否则返回空字符串。$(findstring ,)
filter
函数
模式过滤
字符串,保留符合
模式中的子字符串,支持多个模式过滤。
模式的字符串。$(filter ,)
filter-out
函数
模式过滤
字符串,去除符合
模式中的子字符串,支持多个模式过滤。
模式后的字符串。$(filter-out ,)
sort
函数
字符串中的子字符串进行排序(升序),同时去除字符串中重复的子字符串。$(sort )
word
函数
字符串中第
个子字符串。
字符串中第
个子字符串,若
超出
字符串长度则返回空字符串。$(word ,)
wordlist
函数
字符串中从第
开始到
的子字符串的列表。
字符串中第
到
的子字符串列表,若
超出
字符串长度则返回空字符串,若
超出
字符串长度则返回从
开始的所有子字符串列表。$(wordlist ,,)
words
函数
字符串中的子字符串数量。
字符串中子字符串的数量。$(words )
firstword
函数
字符串的第一个子字符串。
字符串的第一个子字符串。$(firstword )
dir
函数
文件列表中文件路径的目录部分,若提供文件的路径中不存在/
,则返回./
。
文件列表的目录列表。
字符串的第一个子字符串。$(dir )
notdir
函数
文件列表中文件路径的文件名部分。
文件列表的文件名列表。$(notdir )
suffix
函数
文件列表中文件的后缀部分,若不存在后缀则返回空字符串。
文件列表的后缀列表。$(suffix )
basename
函数
文件列表中文件的前缀部分,若不存在前缀则返回空字符串。
文件列表的前缀列表。$(basename )
addsuffix
函数
文件列表中文件添加后缀部分。
文件列表添加后缀后的列表。$(addsuffix ,)
addprefix
函数
文件列表中文件添加前缀部分。
文件列表添加前缀后的列表。$(addprefix ,)
join
函数
中的子字符串按顺序链接到
中对应的子字符串,若
的子字符串个数与
的子字符串个数不相等,则多出来的子字符串将保持原样不进行链接。$(join ,)
realpath
函数
文件列表的文件相对路径转换成绝对路径,当提供的文件为链接时会进行解析其所链接的文件路径。$(realpath )
abspath
函数
文件列表的文件相对路径转换成绝对路径,当提供的文件为链接时不会进行解析其所链接的文件路径。$(abspath )
wildcard
函数
模式的文件,使用通配符*
来匹配任意字符。$(wildcard )
foreach
函数
字符串中的每一个子字符串(以空格分隔开来),将其赋值到
变量并执行
操作作为结果。$(foreach ,,)
获取执行make test
命令的路径下所有以.c结尾的文件,并在每个文件后面增加.o:
FILES = $(wildcard *.c)
test :
@echo $(foreach n,$(FILES),$(n).o)
if
函数
条件为非空字符串,则条件为真执行
,否则执行
。
条件为真,则返回
的值,否则返回
的值。若返回
时未定义则返回空字符串。$(if ,[,])
shell
函数
命令当做一条shell命令来执行,并返回shell命令的执行结果。$(shell )
利用上面的函数来编写一个Makefile文件,实现自动将执行make
命令的路径下的所有源文件.c文件进行编译链接,若不设置输出的可执行文件名则默认设置为该路径的文件夹名:
TARGET ?= $(subst $(realpath ..)/,,$(realpath .))
ALLSRC = $(foreach SRC,$(shell find . -name "*.c"),$(notdir $(SRC)))
OBJS = $(ALLSRC:.c=.o)
CC = gcc
$(TARGET) : $(OBJS)
$(CC) $^ -o $@
%.o : %.c
$(CC) -c $< -o $@
clean :
rm -rf *.o $(TARGET)
在上面由我们编写的Makefile文件中的各个生成规则,称为显式规则。除了显式规则,Makefile文件还包括隐式规则,它由make自动推导出来不需要用户进行编写。隐式规则只能用于推导对象文件.o和源文件.c之间的依赖关系,当Makefile的某一目标文件依赖于某个对象文件,但没有对应的显式规则时make就会自动添加一个隐式规则:
main : main.o add.o
gcc main.o add.o -o main
对于上面这样一个Makefile文件,我们并没有编写生成对象文件.o的规则,但是当我们执行make
命令的时候仍然能够完成编译生成可执行文件,并且我们可以看到在终端中显示了cc -c -o main.o main.c
和cc -c -o add.o add.c
这两条指令,这就是Makefile的隐式规则。隐式规则中使用的变量分为两种:一种是命令相关的,一种是参数相关的。
变量 | 含义 |
---|---|
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" |
WEAVE | 转化Web到TeX的程序。默认命令是"weave" |
TEXI2DVI | 从Texinfo源文件创建TeX DVI文件的程序。默认命令是"texi2dvi" |
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文法分析器参数 |