使用Makefile编写规则编写一个输出Hello world的程序,程序文件如下:
$ cat print.h
#include
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.o
cc -o helloworld main.o print.o
mian.o : mian.c print.h
cc -c main.c
print.o : print.c print.h
cc -c print.c
clean :
rm helloworld main.o print.o
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
就会执行后续定义的命令。
下文所说的helloworld目标,指的是Makefile中的target,helloworld文件 指的是项目中名为helloworld的文件,注意区分。在默认的方式下,也就是我们只输入make命令,那么:
.o
目标文件的文件修改时间新(条件2),且.o
目标文件比其所依赖的.c
和.h
文件的文件修改时间新(条件3),则make啥也不做.o
目标中定义的命令,都会执行。.o
目标有两个,哪个不满足条件就执行哪个目标定义的命令,满足的那个不执行。这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
在Makefile示例中,main.o
和print.o
在prerequisites和command总共出现了3次,如果再有新的xxx.o
规则,在这三个地方都要加。所以,为了makefile的易维护,在makefile中我们可以使用变量。如下就是对变量OBJECTS
的定义和使用:
OBJECTS = main.o print.o
helloworld: $(OBJECTS)
cc -o helloworld $(OBJECTS)
mian.o: mian.c print.h
cc -c main.c
print.o: print.c print.h
cc -c print.c
clean:
rm helloworld $(OBJECTS)
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.o
helloworld: $(OBJECTS)
cc -o helloworld $(OBJECTS)
mian.o:
print.o:
clean:
rm helloworld $(OBJECTS)
甚至还可以按照下面方式写,不过这样Makefile文件依赖关系就显得有点凌乱了,可以根据实际情况选择,别人写了要能看懂。
OBJECTS = main.o print.o
helloworld: $(OBJECTS)
cc -o helloworld $(OBJECTS)
$(OBJECTS):
clean:
rm helloworld $(OBJECTS)
在 Makefile 中的定义的变量,就像是 C/C++语言中的宏一样,他代表了一个文本字符串,在 Makefile 中执行的时候其会自动原模原样地展开在所使用的地方。其与 C/C++所不同的是,你可以在 Makefile 中改变其值。
变量的命名字可以包含字符、数字,下划线(可以是数字开头),但不应该含有:
、#
、 =
或是空字符
(空格、回车等)。变量是大小写敏感的, foo
、 Foo
和FOO
是三个不同的变量名。
定义或改变一个变量常用以下4种等号:
:=
:立即变量。对于右边引用的变量,在定义左边变量时就会展开,且只有到目前为止定义过的变量才会得到展开。=
:延迟变量。对于右边引用的变量,在执行含有左边变量的命令时才会展开,而不是在定义左边变量时展开。?=
:如果左边变量之前没有被定义过,那么变量的值就是右边值,如果变量先前被定义过,那么这条语将什么也不做。+=
:将等号右边的值追加到左边变量中。如果变量之前没有定义过,那么,+=
会自动变成=
;如果前面有变量定义,那么+=
会继承于前次操作的赋值符;如果前一次的是:=
,那么+=
会以:=
作为其赋值符。变量可以使用在许多地方,如规则中的“目标”、“依赖”、“命令”以及“变量的值”中。引用一个变量可使用$()
、${}
这两个符号,使用第一个居多。
one = hello
#不允许将变量自己的值赋给自己,因为one会递归引用自身
#one = ${one} world
#这样写就允许,不会递归引用
one := ${one} world #这里加注释后,one的值为"hello world ",后面多一个空格
all:
echo $(one)
第一种是变量值的替换,其格式为$(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)))
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 $$blah
define two
export blah=set
echo $$blah
endef
all:
@echo "这会打印 'I was set'"
@$(one)
@echo "这不会打印 'I was set' 因为每个command都在单独的shell中运行"
@$(two)
除了用户自定义的一些变量,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 = cool
all: # 输出coll
@echo $(one)
other: # 输出boy
@echo $(one)
自动变量是局部变量,作用域范围在当前的规则内,它们分别代表不同的含义:
-
:告诉make在编译时忽略所有的错误@
:告诉make在执行命令前不要显示命令$@
:所有目标文件$^
:所有目标依赖$<
:目标依赖列表中的第一个依赖$?
:所有目标依赖中被修改过的文件$%
:当规则的目标是一个静态库文件时,$%
代表静态库的一个成员名$+
:类似$^
,但是保留了依赖文件中重复出现的文件$*
:在模式匹配和静态模式规则中,代表目标模式中%
的部分。比如hello.c
,当匹配模式为%.c
时,$*
表示hello
$(@D)
:表示目标文件的目录部分$(@F)
:表示目标文件的文件名部分$(*D)
:在模式匹配中,表示目标模式中%的目录部分$(*F)
:在模式匹配中,表示目标模式中%的文件名部分在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: %.c
gcc -c $<
# 删除当前目录下所有以.o结尾的文件
clean:
rm -f *.o
默认目标:一个Makefile文件里通常会有多个目标,一般会选择第一个作为默认目标。
多目标:一个规则中也可以有多个目标,多个目标具有相同的生成命令和依赖文件。如一个目标文件%.o
都是由其对应的源文件%.c
编译生成的,生成命令也是相同的。
%.o: %.c
gcc -o %.o %.c
多规则目标:多个规则可能是同一个目标,make在解析Makefile文件时,会将具有相同目标的规则的依赖文件合并。如果每个相同目标后跟一个冒号:
,则多个目标只能有一个目标有执行命令,否则会报错;如果每个相同目标后跟双冒号::
,则多个目标能有多个执行命令。
# 单冒号,只能一个目标有命令
helloworld: main.o
cc -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
命令自动生成头文件依赖关系。ifeq
关键字用来判断两个参数是够相等,相等时条件成立为true,不相等为false。ifeq
一般和变量结合使用:
mode = debug
all:
ifeq ($(mode),debug)
@echo "debug mode"
else
@echo "release mode"
endif
ifneq
关键字和ifeq
关键字恰恰相反,用来判断参数是否不相等。当比较的参数不相等时,条件语句才成立,值为true,否则为false。
ifdef
关键字用来判断一个变量是否已经定义,如果变量的值非空(在Makefile中,没有定义的变量的值为空),表达式为true。
mode = debug
all:
ifdef mode
@echo "def mode"
else
@echo "ndef mode"
endif
ifndef
关键字和ifdef
相反,如果一个变量没有定义,表达式为true。ifdef
和ifndef
后面直接跟变量名,不用引用符号。
关于函数的使用格式,有以下需要注意的地方:
call
函数来间接调用。make内嵌的函数调用语法如下:
$(<function> <arg1>,<arg2>,...)
# 或者
${ ,,...}
GNU make提供了一系列文本处理函数:
$(subst old,new,text)
$(patsubst pattern,replacement,text)
$(strip text)
$(findstring find,text)
$(filter pattern…,text)
$(filer-out pattern…,text)
$(sort text)
$(word n,text)
$(wordlist n,m,text)
$(words text)
$(firstword text)
$(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))
GNU make提供了一系列对文件名进行各种操作的函数:文件名替换、加前缀、去目录等。
$(dir NAMES…)
$(notdir NAMES…)
$(suffix NAMES…)
.
开始(包括点号)的部分。若文件名没有后缀,suffix函数则返回空。$(basename NAMES…)
$(addsuffix SUFFIX,NAMES…)
$(addprefix PREFIX,NAMES…)
$(join LIST1,LIST2)
$(wildcard PATTERN)
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)
如果想做一些循环或遍历操作时,可以使用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)
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)
用户自定义函以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
.PHONY: all
param = $(1) $(2)
str1 = $(call param, hello, zhaixue.cc)
all:
@echo $(str1)
origin
函数的使用格式为:$(origin
如果变量没有定义,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
如果你想在Makefile中运行shell
命令,可以使用shell
函数来完成这个功能。shell
函数的参数是shell
命令,它和反引号
具有相同的功能。shell
命令的运行结果即为shell
函数的返回值。
.PHONY: all
all:
@echo $(shell pwd)
@echo $(shell ls -m)
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..."