前言
本文为《跟我一起写makefile》读书笔记,是自己在学makefile时候自己感觉比较全面的学习资料。
关于程序的编译和链接
概述
-
makefile关系到了整个工程的编译规则。
-
一个工程中的源文件不计其数,并且按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
-
“自动化编译”—只要一个make命令,整个工程就能完全自动编译。
关于程序的编译和链接
- 首先要把源文件编译成中间代码文件,在Windows下也就是
.obj
文件,UNIX下是.o
文件,即Object File,这个动作叫做==编译(compile)。==- 然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。链接时,主要是链接函数和全局变量。链接时,主要是链接函数和全局变量。
- 在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便。所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是==
.lib
文件==,在UNIX下,是Archive File,也就是==.a
文件==。
🔨总结:**源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。**在编译时,编译器只检测程序语法和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码(Linker Error),在VC下,这种错误一般是: Link 2001错误
,意思说是说,链接器未能找到函数的实现。你需要指定函数的Object File。
maekfile介绍
makefile介绍
- 编译和链接的规则:
- 1.如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
- 2.如果这个工程的==某几个c文件被修改,那么我们只编译==被修改的c文件,并链接目标程序。
- 3.如果这个工程的==头文件被改变==了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。
makefile规则
target ... : prerequisites ...
command
...
...
-
target[目标]:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。
-
prerequisites[依赖]:生成该target所依赖的文件和/或target
-
command[命令]:该target要执行的命令(任意的shell命令)
-
makefile规则:
target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。也就是说prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
示例
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
- 反斜杠(
\
)是换行符的意思。这样比较便于makefile的阅读。 - 我们可以把这个内容保存在名字为“makefile”或“Makefile”的文件中,然后在该目录下直接输入命令
make
就可以生成执行文件edit。 - 如果要删除执行文件和所有的中间目标文件,那么,只要简单地执行一下
make clean
就可以了。 - 生成目标文件的操作系统命令,一定要以一个 ==
Tab
==键作为开头。 - make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
clean
不是一个文件,它只不过是一个动作名字,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。
make是如何工作的
输入make命令:
- 1.make会在当前目录下找名字叫**“Makefile”或“makefile”**的文件。
- 2.如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
- 3.如果edit文件不存在,或是edit所依赖的后面的
.o
文件的文件修改时间要比edit
这个文件新,那么,他就会执行后面所定义的命令来生成edit
这个文件。 - 4.如果
edit
所依赖的.o
文件也不存在,那么make会在当前文件中找目标为.o
文件的依赖性,如果找到则再根据那一个规则生成.o
文件。(这有点像一个堆栈的过程) - 5.当然,你的C文件和H文件是存在的啦,于是make会生成
.o
文件,然后再用.o
文件生成make的终极任务,也就是执行文件edit
了。
makefile中使用变量
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
- 如果我们的工程需要加入一个新的
.o
文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。- 为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。
- 我们==声明一个变量==,叫
objects
,OBJECTS
,objs
,OBJS
,obj
或是OBJ
,够表示obj文件。就可以很方便地在我们的makefile中以 ==$(objects)
的方式来使用这个变量==了
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : main.c defs.h
cc -c main.c
...
让makefile自动推导
-
make很强大,它可以自动推导文件以及文件依赖关系后面的命令
-
只要make看到一个
.o
文件,它就会自动的把.c
文件加在依赖关系中,如果make找到一个whatever.o
,那么whatever.c
就会是whatever.o
的依赖文件。并且cc -c whatever.c
也会被推导出来. -
.PHONY
表示clean
是个伪目标文件。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
合并同类项
让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)
清空目标文件的规则
-
每个Makefile中都应该写一个清空目标文件(
.o
和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。 -
.PHONY
表示clean
是一个“伪目标”。而在rm
命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。 -
clean
的规则不要放在文件的开头,“clean从来都是放在文件的最后”。
.PHONY : clean
clean :
-rm edit $(objects)
makefile里有什么
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
- 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。
- 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用
#
字符,这个就像C/C++中的//
一样。如果你要在你的Makefile中使用#
字符,可以用反斜杠进行转义,如:\#
。
最后,还值得一提的是,在Makefile中的命令,必须要以 Tab
键开始。
makefile文件名
-
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用==“Makefile”==这个文件名。大多数的make都支持”makefile”和“Makefile”这两种默认文件名。
-
如果要指定特定的Makefile,你可以使用make的
-f
和--file
参数,如:make -f Make.Linux
或make --file Make.AIX
。
引用其他的makefile
- 在Makefile使用
include
关键字可以把别的Makefile包含进来,这很像C语言的#include
,被包含的文件会原模原样的放在当前文件的包含位置。
include <filename>
filename
可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。在
include
前面可以有一些空字符,但是==绝不能是Tab
键开始==。include
和<filename>
可以用一个或多个空格隔开。make命令开始时,会找寻
include
所指出的其它Makefile,并把其内容安置在当前的位置。
- 如果make执行时,有==
-I
或--include-dir
参数==,那么make就会在这个参数所指定的目录下去寻找。- 如果目录
<prefix>/include
(一般是:/usr/local/bin
或/usr/include
)存在的话,make也会去找。如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。
-include <filename>
环境变量MAKEFILES
当前环境中定义了环境变量 MAKEFILES
,那么,make会把这个变量中的值做一个类似于 include
的动作。这个变量中的值是其它的Makefile,用空格分隔。只是,它和 include
不同的是,从这个环境变量中引入的Makefile的“目标”不会起作用
make的工作方式
make工作时的执行步骤如下:(想来其它的make也是类似)
- 1.读入所有的Makefile。
- 2.读入被include的其它Makefile。
- 3.初始化文件中的变量。
- 4.推导隐晦规则,并分析所有规则。
- 5.为所有的目标文件创建依赖关系链。
- 6.根据依赖关系,决定哪些目标要重新生成。
- 7.执行生成命令。
-
1-5步为第一个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
-
,6-7为第二个阶段。
书写规则
规则包含两个部分,一个是依赖关系,一个是生成目标的方法。
在Makefile中,规则的顺序是很重要的,因为,Makefile中只应该有一个最终目标,其它的目标都是被这个目标所连带出来的,所以一定要让make知道你的最终目标是什么。一般来说,定义在Makefile中的目标可能会有很多,但是==第一条规则中的目标将被确立为最终的目标==。如果第一条规则中的目标有很多个,那么,第一个目标会成为最终的目标。make所完成的也就是这个目标。
规则的语法
targets : prerequisites ; command
command
...
-
targets是文件名,以空格分开,可以使用通配符。一般来说,我们的目标基本上是一个文件,但也有可能是多个文件。
-
command是命令行,如果其不与“target:prerequisites”在一行,那么,必须以==
Tab
键开头==,如果和prerequisites在一行,那么可以用分号做为分隔。(见上) -
prerequisites也就是目标所依赖的文件(或依赖目标)。如果其中的某个文件要比目标文件要新,那么,目标就被认为是“过时的”,被认为是需要重生成的。
-
一般来说,make会以UNIX的标准Shell,也就是
/bin/sh
来执行命令。
在规则中使用通配符
当在使用命令行时,有很多时间都用来查找你所需要的文件,如ls find等。 S h e l l提供了一套完整的字符串模式匹配规则,或者称之为元字符,当s h e l l遇到上述字符时,就会把它们当作特殊字符,而不是文件名中的普通字符,这样用户就可以用它们来匹配相应的文件名,这可以称为通配符。
通配符与正则表达式是有区别的,简单来说:通配符是用来通配的,正则表达式是用来匹配字符串的;
在文本过滤工具里,都是用正则表达式,比如像awk,sed,等,是针对文件的内容的。
而通配符多用在文件名上,比如查找find,ls,cp,等等。
- make支持三个通配符:
*
,?
和~
- 波浪号(
~
)字符在文件名中也有比较特殊的用途。如果是~/test
,这就表示当前用户的$HOME
目录下的test目录。 - 通配符
*
代替了你一系列的文件,如*.c
表示所有后缀为c的文件。 - 转义字符
\
,如\*
来表示真实的*
字符,而不是任意长度的字符串。
clean:
cat main.c
rm -f *.o
在编译完后看看main.c的源代码
objects = *.o
print: *.c
lpr -p $?
touch print
通配符也可以在我们的规则中,也样可以用在变量中。
-
在Makefile规则中,通配符会被自动展开。但在变量的定义和函数引用时,通配符将失效。这种情况下如果需要通配符有效,就需要使用函数“wildcard”,它的用法是:$(wildcard PATTERN…) 。
objects := $(wildcard *.o)
-
一个例子
- wildcard : 扩展通配符
- notdir : 去除路径
- patsubst :替换通配符
-
建立一个测试目录
|----------+ test
|------------| a.c
|------------|b.c
|------------+ sub
|---------------|sa.c
|---------------|sb.c
src=$(wildcard *.c ./sub/*.c) dir=$(notdir $(src)) obj=$(patsubst %.c,%.o,$(dir) ) all: @echo $(src) @echo $(dir) @echo $(obj) @echo "end"
输出
a.c b.c ./sub/sa.c ./sub/sb.c #wildcard把 指定目录 ./ 和 ./sub/ 下的所有后缀是c的文件全部展开。 a.c b.c sa.c sb.c #notdir把展开的文件去除掉路径信息 a.o b.o sa.o sb.o #在$(patsubst %.c,%.o,$(dir) )中,patsubst把$(dir)中的变量符合后缀是.c的全部替换成.o,任何输出。
或者输出可以使用
obj=$(dir:%.c=%.o)
-
makefile里的替换引用规则:
$(var:a=b) 或 $ {var:a=b}
- 它的含义是把变量var中的每一个值结尾用b替换掉a
文件搜索
-
VPATH
Makefile文件中的特殊变量
VPATH
就是完成这个功能的,如果没有指明这个变量,make只会在当前的目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make就会在当前目录找不到的情况下,到所指定的目录中去找寻文件了。VPATH = src:../headers
上面的定义指定两个目录,“src”和“…/headers”,make会按照这个顺序进行搜索。目录由==“冒号”分隔==。(当然,当前目录永远是最高优先搜索的地方
-
vpath
另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。
vpath <pattern> <directories>
为符合模式的文件指定搜索目录。
vpath <pattern>
清除符合模式的文件的搜索目录。
vpath
清除所有已被设置好了的文件搜索目录。
vapth使用方法中的需要包含
%
字符。%
的意思是匹配零或若干字符,(需引用%
,使用\
)例如,%.h
表示所有以.h
结尾的文件。**指定了要搜索的文件集,而则指定了< pattern>的文件集的搜索的目录。**例如:vpath %.c foo:bar vpath % blish
.c
结尾的文件,先在“foo”目录,然后是“bar”目录,最后才是“blish”目录。
伪目标
-
我们也应该提供一个清除它们的“目标”以备完整地重编译而用。 (以“make clean”来使用该目标)因为,我们并不生成“clean”这个文件。“伪目标”并不是一个文件**,只是一个标签**,由于“伪目标”不是文件,所以make无法生成它的依赖关系和决定它是否要执行。我们只有通过显式地指明这个“目标”才能让其生效。当然,“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
.PHONY : clean clean : rm *.o temp
-
```ma
.PHONY : cleanall cleanobj cleandiffcleanall : cleanobj cleandiff
rm programcleanobj :
rm *.ocleandiff :
rm *.diff“make cleanall”将清除所有要被清除的文件。“cleanobj”和“cleandiff”这两个伪目标有点像“子程序”的意思。我们可以输入“make cleanall”和“make cleanobj”和“make cleandiff”命令来达到清除不同种类文件的目的。
多目标
Makefile的规则中的目标可以不止一个,其支持多目标,有可能我们的多个目标同时依赖于一个文件,并且其生成的命令大体类似。我们可以使用一个自动化变量 $@
(关于自动化变量,将在后面讲述),这个变量表示着目前规则中所有的目标的集合。
bigoutput littleoutput : text.g
generate text.g -$(subst output,,$@) > $@
等价于
bigoutput : text.g
generate text.g -big > bigoutput
littleoutput : text.g
generate text.g -little > littleoutput
-$(subst output,,$@)
中的$
表示执行一个Makefile的函数,函数名为subst,后面的为参数。这里的这个函数是替换字符串的意思,$@
表示目标的集合,就像一个数组,$@
依次取出目标,并执于命令。
静态模式
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...
- targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
- target-pattern是指明了targets的模式,也就是的目标集模式。
- prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@
目标从$ object中获取, `%.o` 表明要所有以 `.o` 结尾的目标,也就是 `foo.o bar.o` ,也就是变量 `$ object` 集合的模式,而依赖模式 `%.c` 则取模式 `%.o` 的 `%` ,也就是 `foo bar` ,并为其加下 `.c`的后缀,于是,我们的依赖目标就是 `foo.c bar.c` 。而命令中的 `$<` 和 `$@`则是自动化变量, `$<` 表示第一个依赖文件, `$@` 表示目标集(也就是“foo.o bar.o”)。于是,上面的规则展开后等价于下面的规则:
foo.o : foo.c
$(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o : bar.c
$(CC) -c $(CFLAGS) bar.c -o bar.o
自动生成依赖
大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
cc -M main.c
其输出是:
main.o : main.c defs.h
于是由编译器自动生成的依赖关系,这样一来,你就不必再手动书写若干文件的依赖关系,而由编译器自动生成了。需要提醒一句的是,如果你使用GNU的C/C++编译器,你得用 -MM
参数,不然,``-M` 参数会把一些标准库的头文件也包含进来。
书写命令
- make会一按顺序一条一条的执行命令,每条命令的开头必须以
Tab
键开头。 - 在命令行之间中的空格或是空行会被忽略,但是如果该空格或空行是以Tab键开头的,那么make会认为其是一个空命令。
显示命令
-
make会把其要执行的命令行在命令执行前输出到屏幕上。当我们用
@
字符在命令行前,那么,这个命令将不被make显示出来。@echo 正在编译XXX模块......
-
如果没有echo
echo 正在编译XXX模块...... 正在编译XXX模块......
-
-
带入make参数==
-n
或--just-print
== ,那么其只是显示命令,但不会执行命令.这个功能很有利于我们调试我们的Makefile. -
make参数
-s
或--silent
或--quiet
则是全面禁止命令的显示。
命令执行
-
如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就**不能把这两条命令写在两行上**,而应该把这两条命令写在一行上,用分号分隔。
exec: cd /home/hchen pwd
cd没有作用,pwd会打印出当前的makefile目录
exec: cd /home/hchen; pwd
cd就起作用了,pwd会打印出“/home/hchen”。
命令出错
-
每当命令运行完后,make会检测每个命令的返回码,如果命令返回成功,那么make会执行下一条命令,当规则中所有的命令成功返回后,这个规则就算是成功完成了。如果一个规则中的某个命令出错了(命令退出码非零),那么make就会终止执行当前规则,这将有可能终止所有规则的执行。
-
命令的出错并不表示就是错误的。为了忽略命令的出错,我们可以在Makefile的命令行前加一个减号
-
(在Tab键之后),标记为不管命令出不出错都认为是成功的。clean: -rm -f *.o
-
给make加上
-i
或是--ignore-errors
参数,那么,Makefile中所有命令都会忽略错误。 -
如果一个规则是以==
.IGNORE
作为目标的==,那么这个规则中的所有命令将会忽略错误。 -
make的参数的是
-k
或是--keep-going
,这个参数的意思是,如果某规则中的命令出错了,那么就终止该规则的执行,但继续执行其它规则。
嵌套执行make
-
把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。
-
我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:
subsystem: cd subdir && $(MAKE)
或:
subsystem: $(MAKE) -C subdir
- 定义**$(MAKE)宏变量的意思是,也许我们的make需要一些参数**,所以定义成一个变量比较利于维护。
- 先进入“subdir”目录,然后执行make命令。
-
如果你要传递变量到下级Makefile中,那么你可以使用这样的声明:
export <variable ...>; export variable = value variable = value export variable export variable := value export variable += value #+= 是添加等号后面的值
-
如果你不想让某些变量传递到下级Makefile中,那么你可以这样声明:
unexport <variable ...>;
-
有两个变量,这两个变量不管你是否export,其总是要传递到下层 Makefile中,
- 一个是
SHELL
- 一个是
MAKEFLAGS
,其中包含了make的参数信息,如果我们执行“总控Makefile”时有make参数或是在上层 Makefile中定义了这个变量,那么MAKEFLAGS
变量将会是这些参数,并会传递到下层Makefile中,这是一个系统级的环境变量。
- 一个是
定义命令包
Makefile中出现一些相同命令序列,那么我们可以为这些相同的命令序列定义一个变量。定义这种命令序列的语法以 define
开始,以 endef
结束,
define run-yacc
yacc $(firstword $^)
mv y.tab.c $@
endef
“run-yacc”是这个命令包的名字,其不要和Makefile中的变量重名。在
define
和endef
中的两行就是命令序列。这个命令包中的第一个命令是运行Yacc程序,因为Yacc程序总是生成“y.tab.c”的文件,所以第二行的命令就是把这个文件改改名字。
使用变量
变量的基础
-
变量在声明时需要给予初值,而在使用时,需要给在变量名前加上
$
符号,但最好用小括号==()
或是大括号{}
==把变量给包括起来。如果你要使用真实的$
字符,那么你需要用$$
来表示。 -
变量会在使用它的地方精确地展开,就像C/C++中的宏一样
-
```makefile
foo = c
prog.o : prog.$(foo)
( f o o ) (foo) (foo)(foo) - ( f o o ) p r o g . (foo) prog. (foo)prog.(foo)展开后 ```makefile prog.o : prog.c cc -c prog.c
变量中的变量
-
这个功能有好的地方,也有不好的地方,好的地方是,我们可以把变量的真实值推到后面来定义。
CFLAGS = $(include_dirs) -O include_dirs = -Ifoo -Ibar
-
这种形式也有不好的地方,那就是递归定义。
CFLAGS = $(CFLAGS) -O
这种方式会让我们的make运行时非常慢,他会使用得两个make的函数“wildcard”和“shell”发生不可预知的错误。
:=
-
为了避免上面的这种方法,我们可以使用make中的另一种用变量来定义变量的方法。这种方法使用的是
:=
操作符,这种方法**,前面的变量不能使用后面的变量**,只能使用前面已定义好了的变量。x := foo y := $(x) bar #y := foo bar x := later #x := later
-
错误示范
y := $(x) bar x := foo #y = bar
-
-
如果我们要==定义一个变量,其值是一个空格,==那么我们可以这样来:
nullstring := space := $(nullstring) # end of the line
nullstring是一个Empty变量,其中什么也没有,而我们的space的值是一个空格。因为在操作符的右边是很难描述一个空格的,这里采用的技术很管用,先用一个Empty变量来标明变量的值开始了,而后面采用**“#”注释符来表示变量定义的终止**,这样,我们可以定义出其值是一个空格的变量。
-
错误示范:
dir := /foo/bar # directory to put the frobs in
dir这个变量的值是“/foo/bar”,后面还跟了4个空格,如果我们这样使用这样变量来指定别的目录——“$(dir)/file”那么就完蛋了。
-
?=
FOO ?= bar
如果FOO没有被定义过,那么变量FOO的值就是“bar”,如果FOO先前被定义过,那么这条语将什么也不做,其等价于:
ifeq ($(origin FOO), undefined)
FOO = bar
endif
变量高级用法
变量值的替换
替换变量中的共有的部分,其==格式是 $(var:a=b)
或是 ${var:a=b}
==,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。
foo := a.o b.o c.o
bar := $(foo:.o=.c)
定义了一个
$(foo)
变量,而第二行的意思是把$(foo)
中所有以.o
字串“结尾”全部替换成.c
,所以我们的$(bar)
的值就是“a.c b.c c.c”。
- 以静态模式替换变量
foo := a.o b.o c.o
bar := $(foo:%.o=%.c)
把变量的值再当成变量
x = y
y = z
a := $($(x)) #$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。
这里的
$($(x))
被替换成了$($(y))
,因为$(y)
值是“z”,所以,最终结果是:a:=$(z)
,也就是“Hello”。
x = variable1
variable2 := Hello
y = $(subst 1,2,$(x))
z = y
a := $($($(z)))
$($($(z)))
扩展为$($(y))
,而其再次被扩展为$($(subst 1,2,$(x)))
。$(x)
的值是“variable1”,subst函数把“variable1”中的所有“1”字串替换成“2”字串,于是,“variable1”变成 “variable2”,再取其值,所以,最终,$(a)
的值就是$(variable2)
的值——“Hello”。(喔,好不容易)
$(subst ;,;,
😉 名称:字符串替换函数——subst。
功能:把字串
;中的;字符串替换成;。 返回:函数返回被替换过后的字符串。
追加变量值
-
+=
操作符给变量追加值,objects = main.o foo.o bar.o utils.o objects += another.o
-
如果变量之前没有定义过,那么,
+=
会自动变成=
,如果前面有变量定义,那么+=
会继承于前次操作的赋值符。 -
如果前一次的是
:=
,那么+=
会以:=
作为其赋值符,variable := value variable += more
-
由于前次的赋值符是
=
,所以+=
也会以=
来做为赋值,variable = value variable += more
override提示符
如果有变量是通常make的命令行参数设置的,那么Makefile中对这个变量的赋值会被忽略。如果你想在Makefile中设置这类参数的值,那么,你可以使用“override”指示符。
override <variable>; = <value>;
override <variable>; := <value>;
override <variable>; += <more text>;
环境变量
-
make运行时的系统环境变量可以在make开始运行时被载入到Makefile文件中,但是如果Makefile中已定义了这个变量,或是这个变量由make命令行带入,那么系统的环境变量的值将被覆盖。(如果make指定了“-e”参数,那么,系统环境变量将覆盖Makefile中定义的变量)
-
当make嵌套调用时(参见前面的“嵌套调用”章节),上层Makefile中定义的变量会以系统环境变量的方式传递到下层的Makefile 中。当然,默认情况下,只有通过命令行设置的变量会被传递。而定义在文件中的变量,如果要向下层Makefile传递,则需要使用exprot关键字来声明。
使用条件判断
-
ifeq
、else
和endif
。-
ifeq
的意思表示条件语句的开始,并指定一个条件表达式,表达式包含两个参数,以逗号分隔,表达式以圆括号括起。 -
else
表示条件表达式为假的情况。 -
endif
表示一个条件语句的结束,任何一个条件表达式都应该以endif
结束。ifeq (<arg1>, <arg2>) ifeq '<arg1>' '<arg2>' ifeq "<arg1>" "<arg2>" ifeq "<arg1>" '<arg2>' ifeq '<arg1>' "<arg2>"
-
-
ifneq
:比较参数arg1
和arg2
的值是否相同,不同则为真。ifneq (<arg1>, <arg2>) ifneq '<arg1>' '<arg2>' ifneq "<arg1>" "<arg2>" ifneq "<arg1>" '<arg2>' ifneq '<arg1>' "<arg2>"
-
ifdef
:ifdef
只是测试一个变量是否有值,其并不会把变量扩展到当前位置。 -
make是在读取Makefile时就计算条件表达式的值,并根据条件表达式的值来选择语句,所以,你最好不要把==自动化变量(如
$@
等)==放入条件表达式中,因为自动化变量是在运行时才有的。
使用函数
函数的调用语法
-
函数调用,很像变量的使用,也是以
$
来标识的,$(<function> <arguments>) ${<function> <arguments>}
<function>
就是函数名,make支持的函数不多。<arguments>
为函数的参数,参数间以逗号,
分隔,而函数名和参数之间以==“空格”==分隔。函数调用以$
开头,以圆括号或花括号把函数名和参数括起。
字符串处理函数
subst
$(subst <from>,<to>,<text>)
-
名称:字符串替换函数
-
功能:把==字串
<text>
中的<from>
字符串替换成<to>
== 。 -
返回:函数返回被替换过后的字符串。
-
$(subst ee,EE,feet on the street)
把
feet on the street
中的ee
替换成EE
,返回结果是fEEt on the strEEt
。
patsubst
$(patsubst <pattern>,<replacement>,<text>)
-
名称:模式字符串替换函数。
-
功能:查找
<text>
中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式<pattern>
,如果匹配的话,则以<replacement>
替换。这里,<pattern>
可以包括通配符%
,表示任意长度的字串。如果<replacement>
中也包含%
,那么,<replacement>
中的这个%
将是<pattern>
中的那个%
所代表的字串。(可以用\
来转义,以\%
来表示真实含义的%
字符) -
返回:函数返回被替换过后的字符串。
-
$(patsubst %.c,%.o,x.c.c bar.c)
把字串
x.c.c bar.c
符合模式%.c
的单词替换成%.o
,返回结果是x.c.o bar.o
-
$(var:=😉
strip
$(strip <string>)
- 名称:去空格函数。
- 功能:去掉
<string>
字串中 ,开头和结尾的空字符,并将中间的多个连续空字符(如果有的话)合并为一个空字符。空字符包括 空格,tab等不可显示的字符 - 返回:返回被去掉空格的字符串值。
findstring
$(findstring <find>,<in>)
-
名称:查找字符串函数
-
功能:在字串
<in>
中查找<find>
字串。 -
返回:如果找到,那么返回
<find>
,否则返回空字符串。 -
$(findstring a,a b c) $(findstring a,b c)
第一个函数返回
a
字符串,第二个返回空字符串
filter
$(filter <pattern...>,<text>)
-
名称:过滤函数
-
功能:以
<pattern>
模式过滤<text>
字符串中的单词,保留符合模式<pattern>
的单词。可以有多个模式。 -
返回:返回符合模式
<pattern>
的字串。 -
sources := foo.c bar.c baz.s ugh.h foo: $(sources) cc $(filter %.c %.s,$(sources)) -o foo
$(filter %.c %.s,$(sources))
返回的值是foo.c bar.c baz.s
。
filter-out
$(filter-out <pattern...>,<text>)
-
名称:反过滤函数
-
功能:以
<pattern>
模式过滤<text>
字符串中的单词,去除符合模式<pattern>
的单词。可以有多个模式。 -
返回:返回不符合模式
<pattern>
的字串。 -
objects=main1.o foo.o main2.o bar.o mains=main1.o main2.o
$(filter-out $(mains),$(objects))
返回值是foo.o bar.o
。
sort
$(sort <list>)
-
名称:排序函数
-
功能:给字符串
<list>
中的单词排序(升序)。 -
返回:返回排序后的字符串。
-
备注:
sort
函数会去掉<list>
中相同的单词。 -
`$(sort foo bar lose)`
返回
bar foo lose
。
word
$(word <n>,<text>)
-
名称:取单词函数
-
功能:取字符串
<text>
中第<n>
个单词。(从一开始) -
返回:返回字符串
<text>
中第<n>
个单词。如果<n>
比<text>
中的单词数要大,那么返回空字符串。 -
$(word 2, foo bar baz)
返回值是
bar
。
wordlist
$(wordlist <ss>,<e>,<text>)
-
名称:取单词串函数
-
功能:从**字符串
<text>
中取从<ss>
开始到<e>
的单词串**。<ss>
和<e>
是一个数字。 -
返回:返回字符串
<text>
中从<ss>
到<e>
的单词字串。如果 **<ss>
比<text>
中的单词数要大,那么返回空字符串。**如果<e>
大于<text>
的单词数,那么返回从<ss>
开始,到<text>
结束的单词串。 -
$(wordlist 2, 3, foo bar baz)
返回值是
bar baz
words
$(words <text>)
-
名称:单词个数统计函数
-
功能:统计
<text>
中字符串中的单词个数。 -
返回:返回
<text>
中的单词数。 -
备注:如果我们要取
<text>
中最后的一个单词,我们可以这样:$(word $(words <text>),<text>)
-
$(words, foo bar baz)
返回值是
3
。
firstword
$(firstword <text>)
- 名称:首单词函数——firstword。
- 功能:取字符串
<text>
中的第一个单词。 - 返回:返回字符串
<text>
的第一个单词。 - 备注:这个函数可以用
word
函数来实现:$(word 1,<text>)
。
文件名操作函数
dir
$(dir <names...>)
-
名称:取目录函数——dir。
-
功能:从文件名序列
<names>
中取出目录部分。目录部分是指最后一个反斜杠(/
)之前的部分。如果**没有反斜杠,那么返回./
** 。 -
返回:返回文件名序列
<names>
的目录部分。 -
$(dir src/foo.c hacks)
返回值是
src/ ./
。
notdir
$(notdir <names...>)
-
名称:取文件函数——notdir。
-
功能:从文件名序列
<names>
中取出非目录部分。非目录部分是指最後一个反斜杠(/
)之后的部分。 -
返回:返回文件名序列
<names>
的非目录部分。 -
$(notdir src/foo.c hacks)
返回值是
foo.c hacks
suffix
$(suffix <names...>)
-
名称:取後缀函数——suffix。
-
功能:从==文件名序列
<names>
==中取出各个文件名的**后缀**。 -
返回:返回文件名序列
<names>
的后缀序列,如果文件没有后缀,则返回空字串。 -
$(suffix src/foo.c src-1.0/bar.c hacks)
返回值是
.c .c
basename
$(basename <names...>)
-
名称:取前缀函数——basename。
-
功能:从==文件名序列
<names>
== 中取出各个文件名的**前缀**部分。 -
返回:返回文件名序列
<names>
的前缀序列,如果文件没有前缀,则返回空字串。 -
$(basename src/foo.c src-1.0/bar.c hacks)
返回值是
src/foo src-1.0/bar hacks
。
addsuffix
$(addsuffix <suffix>,<names...>)
-
名称:加后缀函数——addsuffix。
-
功能:把后缀
<suffix>
加到<names>
中的每个单词后面。 -
返回:返回加过后缀的文件名序列。
-
$(addsuffix .c,foo bar)
返回值是
foo.c bar.c
addprefix
$(addprefix <prefix>,<names...>)
-
名称:加前缀函数——addprefix。
-
功能:把前缀
<prefix>
加到<names>
中的每个单词后面。 -
返回:返回加过前缀的文件名序列。
-
$(addprefix src/,foo bar)
返回值是
src/foo src/bar
join
$(join <list1>,<list2>)
-
名称:连接函数——join。
-
功能:把
<list2>
中的单词对应地加到<list1>
的单词后面。如果<list1>
的单词个数要比<list2>
的多,那么,<list1>
中的多出来的单词将保持原样。如果<list2>
的单词个数要比<list1>
多,那么,<list2>
多出来的单词将被复制到<list1>
中。 -
返回:返回连接过后的字符串。
-
$(join aaa bbb , 111 222 333)
返回值是
aaa111 bbb222 333
foreach 函数
$(foreach <var>,<list>,<text>)
-
把参数
<list>
中的单词逐一取出放到参数<var>
所指定的变量中,然后再执行<text>
所包含的表达式。每一次<text>
会返回一个字符串,循环过程中,<text>
的所返回的每个字符串会以空格分隔,最后当整个循环结束时,<text>
所返回的每个字符串所组成的整个字符串(以空格分隔)将会是foreach函数的返回值。 -
names := a b c d files := $(foreach n,$(names),$(n).o)
$(name)
中的单词会被挨个取出,并存到变量n
中,$(n).o
每次根据$(n)
计算出一个值,这些值以空格分隔,最后作为foreach函数的返回,所以,$(files)
的值是a.o b.o c.o d.o
。foreach中的<var>
参数是一个临时的局部变量,foreach函数执行完后,参数<var>
的变量将不在作用,其作用域只在foreach函数当中。
if函数
$(if <condition>,<then-part>)
$(if <condition>,<then-part>,<else-part>)
-
<condition>
参数是if的表达式,如果其返回的为非空字符串,那么这个表达式就相当于返回真,于是,<then-part>
会被计算,否则<else-part>
会被计算。 -
而if函数的返回值是,如果
<condition>
为真(非空字符串),那个<then-part>
会是整个函数的返回值,如果<condition>
为假(空字符串),那么<else-part>
会是整个函数的返回值,此时如果<else-part>
没有被定义,那么,整个函数返回空字串。 -
所以,
<then-part>
和<else-part>
只会有一个被计算。
call函数
$(call <expression>,<parm1>,<parm2>,...,<parmn>)
-
当make执行这个函数时,
<expression>
参数中的变量,如$(1)
、$(2)
等,会被参数<parm1>
、<parm2>
、<parm3>
依次取代。而<expression>
的返回值就是 call 函数的返回值。 -
reverse = $(1) $(2) foo = $(call reverse,a,b)
foo
的值就是a b
。当然,参数的次序是可以自定义的,reverse = $(2) $(1) foo = $(call reverse,a,b)
此时的
foo
的值就是b a
。
origin函数
$(origin <variable>)
-
Origin函数会以其返回值来告诉你这个变量的“出生情况”,下面,是origin函数的返回值:
-
undefined
如果
<variable>
从来没有定义过,origin函数返回这个值undefined
-
default
如果
<variable>
是一个默认的定义,比如“CC”这个变量, -
environment
如果
<variable>
是一个环境变量,并且当Makefile被执行时,-e
参数没有被打开。 -
file
如果
<variable>
这个变量被定义在Makefile中。 -
command line
如果
<variable>
这个变量是被命令行定义的。 -
override
如果
<variable>
是被override指示符重新定义的。 -
automatic
如果
<variable>
是一个命令运行中的自动化变量。
-
shell函数
contents := $(shell cat foo)
files := $(shell echo *.c)
- 这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。
控制make函数
$(error <text ...>)
- 产生一个致命的错误,
<text ...>
是错误信息。注意,error函数不会在一被使用就会产生错误信息,所以如果你把其定义在某个变量中,并在后续的脚本中使用这个变量,那么也是可以的。
make的运行
make命令会找当前目录的makefile来执行,一切都是自动的。
不同的时候使用不同的编译规则,
make的退出码
make命令执行后有三个退出码:
- 0:表示成功执行。
- 1:如果make运行时出现任何错误,其返回1。
- 2:如果你使用了make的“-q”选项,并且make使得一些目标不需要更新,那么返回2。
指定make
-
make找寻默认的Makefile的规则是在当前目录下依次找三个文件——“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件,一旦找到,就开始读取这个文件并执行。
-
我们也可以给make命令指定一个特殊名字的Makefile。要使用make的
-f
或是--file
参数(--makefile
参数也行)make –f hchen.mk
指定目标
-
一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。
-
可以指示make,让其完成你所指定的目标。任何在makefile中的目标都可以被指定成终极目标,但是除了以
-
打头,或是包含了=
的目标,因为有这些字符的目标,会被解析成命令行参数或是变量。 -
有一个make的环境变量叫
MAKECMDGOALS
,这个变量中会存放你所指定的终极目标的列表,如果在命令行上,你没有指定目标,那么,这个变量是空值。sources = foo.c bar.c ifneq ( $(MAKECMDGOALS),clean) include $(sources:.c=.d) endif
只要我们输入的命令不是“make clean”,那么makefile会自动包含“foo.d”和“bar.d”这两个makefile。
检查规则
不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。
-n
,--just-print
,--dry-run
,--recon
不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。
-t
,--touch
这个参数的意思就是把目标文件的时间更新,但不更改目标文件。也就是说,make假装编译目标,但不是真正的编译目标,只是把目标变成已编译过的状态。
-q
,--question
这个参数的行为是找目标的意思,也就是说,如果目标存在,那么其什么也不会输出,当然也不会执行编译,如果目标不存在,其会打印出一条出错信息。
-W <file>
,--what-if=<file>
,--assume-new=<file>
,--new-file=<file>
这个参数需要指定一个文件。一般是是源文件(或依赖文件),Make会根据规则推导来运行依赖于这个文件的命令,一般来说,可以和“-n”参数一同使用,来查看这个依赖文件所发生的规则命令。
隐含规则
自动化变量
下面是所有的自动化变量及其说明:
$@
: 表示规则中的目标文件集。在模式规则中,如果有多个目标,那么,$@
就是匹配于目标中模式定义的集合。$%
: 仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是foo.a(bar.o)
,那么,$%
就是bar.o
,$@
就是foo.a
。如果目标不是函数库文件(Unix下是.a
,Windows下是.lib
),那么,其值为空。$<
: 依赖目标中的第一个目标名字。如果依赖目标是以模式(即%
)定义的,那么$<
将是符合模式的一系列的文件集。注意,其是一个一个取出来的。$?
: 所有比目标新的依赖目标的集合。以空格分隔。$^
: 所有的依赖目标的集合。以空格分隔。如果在依赖目标中有多个重复的,那么这个变量会去除重复的依赖目标,只保留一份。$+
: 这个变量很像$^
,也是所有依赖目标的集合。只是它不去除重复的依赖目标。$*
: 这个变量表示目标模式中%
及其之前的部分。如果目标是dir/a.foo.b
,并且目标的模式是a.%.b
,那么,$*
的值就是dir/a.foo
。这个变量对于构造有关联的文件名是比较有较。如果目标中没有模式的定义,那么$*
也就不能被推导出,但是,如果目标文件的后缀是make所识别的,那么$*
就是除了后缀的那一部分。例如:如果目标是foo.c
,因为.c
是make所能识别的后缀名,所以,$*
的值就是foo
。这个特性是GNU make的,很有可能不兼容于其它版本的make,所以,你应该尽量避免使用$*
,除非是在隐含规则或是静态模式中。如果目标中的后缀是make所不能识别的,那么$*
就是空值。