文章目录
- 什么是 Makefile?
- 规则
- 工作流程
- 通配符的使用
- 变量的定义和使用
- 定义
- 赋值
- 简单赋值
- 递归赋值
- 条件赋值
- 追加赋值
- 自动化变量
- 目标文件搜索(VPATH和vpath)
- VPATH的使用
- vpath的使用
- 路径示例
- 隐含规则
- ifeq、ifneq、ifdef和ifndef(条件判断)
- ifeq 和 ifneq
- ifdef 和 ifndef
- 伪目标
- 常用字符串处理函数
- 常用文件名操作函数
- 其它常用函数
- foreach
- if
- call
- origin
- shell pwd
- lastword
- 命令的编写
- 命令回显
- 命令的执行
- 并发执行命令
参考链接:
http://c.biancheng.net/view/7091.html
Windows 下的集成开发环境(IDE)已经内置了 Makefile
不了解 Makefile 是一件非常失败的事情,甚至说你就成为不了一个合格的 Linux 开发工程师。不懂 Makefile,就操作不了多文件编程,就完成不了相对于大的工程项目的操作。如果你想在 Linux(Unix) 环境下做开发的话,Makefile 是必须掌握的一项技能。
什么是 Makefile?
Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。是否需要编译,先后编译,重建。
Linux 下的C语言开发为例,编译命令
gcc -o outfile name1.c name2.c ...
outfile 要生成的可执行程序的名字,nameN.c 是源文件的名字。
需要我们手动链接的标准库:
- name1.c 用到了数学计算库 math 中的函数,我们得手动添加参数 -Im;
- name4.c 用到了小型数据库 SQLite 中的函数,我们得手动添加参数 -lsqlite3;
- name5.c 使用到了线程,我们需要去手动添加参数 -lpthread。
因为有很多的文件,还要去链接很多的第三方库。所以在编译的时候命令会很长,并且在编译的时候我们可能会涉及到文件链接的顺序问题,所以手动编译会很麻烦。
如果我们学会使用 Makefile 就不一样了,它会彻底简化编译的操作。把要链接的库文件放在 Makefile 中,制定相应的规则和对应的链接顺序。这样只需要执行 make 命令,工程就会自动编译。每次想要编译工程的时候就执行 make ,省略掉手动编译中的参数选项和命令,非常的方便。
如果我们去做项目开发,免不了要去修改工程项目的源文件,每次修改后都要去重新编译。一个大的工程项目可不止有几个的源文件,里面的源文件个数可能有成百上千个。例如一个内核,或者是一个软件的源码包。这些都是我们做开发经常会遇到的。要完成这样的文件的编译,我们消耗的时间可不是一点点。如果文件特别大的话我们可能要花上半天的时间。
对于这样的问题我们 Makefile 可以解决吗?当然是可以的,Makefile 支持多线程并发操作,会极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。
这其实是我们遇到的比较常见的问题,当然可能遇到的问题还会有很多,比如:工程文件中的源文件的类型很多,编译的话需要选择的编译器;文件可能会分布在不同的目录中,使用时需要调价路径。这些问题都可以通过 Makefile 解决。并且文件中的 Makefile 只需要完成一次,一般我们只要不增加或者是删除工程中的文件,Makefile 基本上不用去修改,编译时只用一个 make 命令。为我们提供了极大的便利,很大程度上提高编译的效率。
规则
Makefile 描述的是文件编译的相关规则主要是两个部分组成,分别是依赖的关系和执行的命令。
targets : prerequisites
command
或者
targets : prerequisites; command
command
相关说明如下:
-
targets
:规则的目标,可以是Object File
(一般称它为中间文件),也可以是可执行文件,还可以是一个标签; -
prerequisites
:是我们的依赖文件,要生成 targets 需要的文件或者是目标。可以是多个,也可以是没有; -
command
:make 需要执行的命令(任意的 shell 命令)。可以有多条命令,每一条命令占一行。
注意:我们的目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用Tab
键。
例子
test:test.c
gcc -o test test.c
实现的功能就是编译 test.c
文件,其中 test 是的目标文件,也是我们的最终生成的可执行文件。依赖文件就是 test.c
源文件,重建目标文件需要执行的操作是gcc -o test test.c
。这就是 Makefile 的基本的语法规则的使用。
使用 Makefile
的方式:首先需要编写好 Makefile 文件,然后在 shell 中执行 make 命令,程序就会自动执行,得到最终的目标文件。
Makefile 中的内容主要包含有五个部分:
- 显式规则
显式规则说明了,如何生成一个或多的的目标文件。这是由 Makefile 的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。 - 隐晦规则
由于我们的 make 命名有自动推导的功能,所以隐晦的规则可以让我们比较粗糙地简略地书写 Makefile,这是由 make 命令所支持的。 - 变量的定义
在 Makefile 中我们要定义一系列的变量,变量一般都是字符串,这个有点像C语言中的宏,当 Makefile 被执行时,其中的变量都会被扩展到相应的引用位置上。 - 文件指示
其包括了三个部分,一个是在一个 Makefile 中引用另一个 Makefile,就像C语言中的include
一样;另一个是指根据某些情况指定 Makefile 中的有效部分,就像C语言中的预编译 #if 一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。 - 注释
Makefile 中只有行注释,和 UNIX 的 Shell 脚本一样,其注释是用“#”字符,这个就像 C/C++ 中的“//”一样。如果你要在你的 Makefile 中使用“#”字符,可以用反斜框进行转义,如:“#”。
工作流程
当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。我们编写 Makefile 的时可以使用的文件的名称 “GNUmakefile” 、“makefile” 、“Makefile” ,make 执行时回去寻找 Makefile 文件,找文件的顺序也是这样的。
我们推荐使用 Makefile(一般在工程中都这么写,大写的会比较的规范)。如果文件不存在,make 就会给我们报错,提示:
make:*** 没有明确目标并且找不到 makefile。停止
Makefile 的具体工作流程可以通过例子来看一下:创建一个包含有多个源文件和 Makefile
的目录文件,源文件之间相互关联。在 Makefile 中添加下面的代码:
main:main.o test1.o test2.o
gcc main.o test1.o test2.o -o main
main.o:main.c test.h
gcc -c main.c -o main.o
test1.o:test1.c test.h
gcc -c test1.c -o test1.o
test2.o:test2.c test.h
gcc -c test2.c -o test2.o
在我们编译项目文件的时候,默认情况下,make 执行的是 Makefile 中的第一规则(Makefile 中出现的第一个依赖关系),此规则的第一目标称之为“最终目标”或者是“终极目标”。
在 shell 命令行执行的 make
命令,就可以得到可执行文件 main 和中间文件 main.o
、test1.o
和 test2.o
,main 就是我们要生成的最终文件。通过 Makefile
我们可以发现,目标 main"在 Makefile
中是第一个目标,因此它就是 make
的终极目标,当修改过任何 C 文件后,执行 make
将会重建终极目标 main
。
它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 “main” 所在的规则。规则描述了 “main” 的依赖关系,并定义了链接 “.o” 文件生成目标 “main” 的命令;make 在执行这个规则所定义的命令之前,首先处理目标 “main” 的所有的依赖文件(例子中的那些 “.o” 文件)的更新规则(以这些 “.o” 文件为目标的规则)。
对这些 “.o” 文件为目标的规则处理有下列三种情况:
- 目标 “.o” 文件不存在,使用其描述规则创建它;
- 目标 “.o” 文件存在,目标 “.o” 文件所依赖的 “.c” 源文件 “.h” 文件中的任何一个比目标 ".o"文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成它;
- 目标 “.o” 文件存在,目标 “.o” 文件比它的任何一个依赖文件(".c" 源文件、".h" 文件)“更新”(它的依赖文件在上一次make 之后没有被修改),则什么也不做。
通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 “.o” 文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。小的工程项目可能体会不到,项目工程文件越大,效果才越明显。
当然 make 命令能否顺利的执行,还在于我们是否制定了正确的的依赖规则,当前目录下是不是存在需要的依赖文件,只要任意一点不满足,我们在执行 make 的时候就会出错。所以完成一个正确的 Makefile 不是一件简单的事情。
清除工作目录中的过程文件
我们在使用的时候会产生中间文件会让整个文件看起来很乱,所以在编写 Makefile 文件的时候会在末尾加上这样的规则语句:
.PHONY:clean
clean:
rm -rf *.o test
其中 “*.o” 是执行过程中产生的中间文件,“test” 是最终生成的执行文件。我们可以看到 clean 是独立的,它只是一个伪目标,不是具体的文件。不会与第一个目标文件相关联,所以我们在执行 make 的时候也不会执行下面的命令。在shell 中执行 “make clean
” 命令,编译时的中间文件和生成的最终目标文件都会被清除,方便我们下次的使用。
通配符的使用
Makefile 是可以使用 shell 命令的,所以 shell 支持的通配符在 Makefile 中也是同样适用的。 shell 中使用的通配符有:"*","?","[…]"
通配符 | 使用说明 |
* | 匹配0个或者是任意个字符 |
? | 匹配任意一个字符 |
[] | 我们可以指定匹配的字符放在 “[]” 中 |
.PHONY:clean
clean:
rm -rf *.o test
表示的是任意的以 .o 结尾的文件
test:*.c
gcc -o $@ $^
表示生所有的以 .c 结尾的文件
OBJ=*.c
test:$(OBJ)
gcc -o $@ $^
我们去执行这个命令的时候会出现错误,提示我们没有 “.c" 文件,实例中我们相要表示的是当前目录下所有的 “.c” 文件,但是我们在使用的时候并没有展开,而是直接识别成了一个文件。文件名是 ".c”。
如果我们就是相要通过引用变量的话,我们要使用一个函数 “wildcard”,这个函数在我们引用变量的时候,会帮我们展开。我们把上面的代码修改一下就可以使用了。
OBJ=$(wildcard *.c)
test:$(OBJ)
gcc -o $@ $^
还有一个和通配符 “*” 相类似的字符,这个字符是 “%”,也是匹配任意个字符,使用在我们的的规则当中。
test:test.o test1.o
gcc -o $@ $^
%.o:%.c
gcc -o $@ $^
“%.o” 把我们需要的所有的 “.o” 文件组合成为一个列表,从列表中挨个取出的每一个文件,"%" 表示取出来文件的文件名(不包含后缀),然后找到文件中和 "%"名称相同的 “.c” 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。
这个属于 Makefile 中静态模规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。跟我们的多规则目标的意思相近,但是又不相同。
变量的定义和使用
定义
Makefile 文件中定义变量的基本语法如下:
变量的名称=值列表
Makefile 中的变量的使用其实非常的简单,因为它并没有像其它语言那样定义变量的时候需要使用数据类型。变量的名称可以由大小写字母、阿拉伯数字和下划线构成。等号左右的空白符没有明确的要求,因为在执行 make 的时候多余的空白符会被自动的删除。至于值列表,既可以是零项,又可以是一项或者是多项。如:
VALUE_LIST = one two three
调用变量的时候可以用 “$(VALUE_LIST)
” 或者是 “${VALUE_LIST}
” 来替换,这就是变量的引用。实例:
OBJ=main.o test.o test1.o test2.o
test:$(OBJ)
gcc -o test $(OBJ)
这就是引用变量后的 Makefile 的编写,比我们之前的编写方式要简单的多。当要添加或者是删除某个依赖文件的时候,我们只需要改变变量 “OBJ” 的值就可以了。
赋值
变量的基本赋值
知道了如何定义,下面我们来说一下 Makefile 的变量的四种基本赋值方式:
简单赋值 ( := ) 编程语言中常规理解的赋值方式,只对当前语句的变量有效。
递归赋值 ( = ) 赋值语句可能影响多个变量,所有目标变量相关的其他变量都受影响。
条件赋值 ( ?= ) 如果变量未定义,则使用符号中的值定义变量。如果该变量已经赋值,则该赋值语句无效。
追加赋值 ( += ) 原变量用空格隔开的方式追加一个新值。
简单赋值
x:=foo
y:=$(x)b
x:=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
在 shell 命令行执行make test
我们会看到:
y=>foob
x=>new
递归赋值
x=foo
y=$(x)b
x=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
在 shell 命令行执行make test我们会看到:
y=>newb
x=>new
条件赋值
x:=foo
y:=$(x)b
x?=new
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
在 shell 命令行执行make test我们会看到:
y=>foob
x=>foo
追加赋值
x:=foo
y:=$(x)b
x+=$(y)
test:
@echo "y=>$(y)"
@echo "x=>$(x)"
在 shell 命令行执行make test我们会看到:
y=>foob
x=>foo foob
其实变量在我们的 Makefile 中还是有很多种类的,它们的意义是不相同的。比如我们的环境变量,自动变量,模式指定变量等。
自动化变量
关于自动化变量可以理解为由 Makefile 自动产生的变量。在模式规则中,规则的目标和依赖的文件名代表了一类的文件。规则的命令是对所有这一类文件的描述。我们在 Makefile 中描述规则时,依赖文件和目标文件是变动的,显然在命令中不能出现具体的文件名称,否则模式规则将失去意义。
那么模式规则命令中该如何表示文件呢?就需要使用“自动化变量”,自动化变量的取值根据执行的规则来决定,取决于执行规则的目标文件和依赖文件。下面是对所有的自动化变量进行的说明:
自动化变量 | 说明 |
$@ | 表示规则的目标文件名。如果目标是一个文档文件(Linux 中,一般成 .a 文件为文档文件,也成为静态的库文件),那么它代表这个文档的文件名。在多目标模式规则中,它代表的是触发规则被执行的文件名。 |
$% | 当目标文件是一个静态库文件时,代表静态库的一个成员名。 |
$< | 规则的第一个依赖的文件名。如果是一个目标文件使用隐含的规则来重建,则它代表由隐含规则加入的第一个依赖文件。 |
$? | 所有比目标文件更新的依赖文件列表,空格分隔。如果目标文件时静态库文件,代表的是库文件(.o 文件)。 |
$^ | 代表的是所有依赖文件列表,使用空格分隔。如果目标是静态库文件,它所代表的只能是所有的库成员(.o 文件)名。一个文件可重复的出现在目标的依赖中,变量“^”会去掉重复的依赖文件 |
$+ | 类似“$^”,但是它保留了依赖文件中重复出现的文件。主要用在程序链接时库的交叉引用场合。 |
$* | 在模式规则和静态模式规则中,代表“茎”。“茎”是目标模式中“%”所代表的部分(当文件名中存在目录时, “茎”也包含目录部分)。 |
下面我们就自动化变量的使用举几个例子。
test:test.o test1.o test2.o
gcc -o $@ $^
test.o:test.c test.h
gcc -o $@ $<
test1.o:test1.c test1.h
gcc -o $@ $<
test2.o:test2.c test2.h
gcc -o $@ $<
这个规则模式中用到了 “$@
” 、"$<
" 和 “$^
” 这三个自动化变量,对比之前写的 Makefile 中的命令,我们可以发现 “$@
” 代表的是目标文件test,“$^
”代表的是依赖的文件,“$<
”代表的是依赖文件中的第一个。我们在执行 make 的时候,make 会自动识别命令中的自动化变量,并自动实现自动化变量中的值的替换,这个类似于编译C语言文件的时候的预处理的作用。
lib:test.o test1.o test2.o
ar r $?
假如我们要做一个库文件,库文件的制作依赖于这三个文件。当修改了其中的某个依赖文件,在命令行执行 make 命令,库文件 “lib” 就会自动更新。"$?
" 表示修改的文件。
GNU make
中在这些变量中加入字符 “D” 或者 “F” 就形成了一系列变种的自动化变量,这些自动化变量可以对文件的名称进行操作。
目标文件搜索(VPATH和vpath)
我们之前列举的例子,所有的源文件基本上都是存放在与 Makefile 相同的目录下。只要依赖的文件存在,并且依赖规则没有问题,执行 make命令整个工程就会按照对我们编写规则去编译,最终会重建目标文件。那如果需要的文件是存在于不同的路径下,在编译的时候要去怎么办呢(不改变工程的结构)?这就用到了 Makefile 中为我们提供的目录搜索文件的功能。
常见的搜索的方法的主要有两种:一般搜索VPATH和选择搜索vpath。乍一看只是大小写的区别,其实两者在本质上也是不同的。
VPATH
和 vpath
的区别:VPATH 是变量,更具体的说是环境变量,Makefile
中的一种特殊变量,使用时需要指定文件的路径;vpath 是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件。
VPATH的使用
在 Makefile 中可以这样写:
VPATH := src
我们可以这样理解,把 src 的值赋值给变量 VPATH,所以在执行 make 的时候会从 src 目录下找我们需要的文件。
当存在多个路径的时候我们可以这样写:
VPATH := src car
或者是
VPATH := src:car
多个路径之间要使用空格或者是冒号隔开,表示在多个路径下搜索文件。搜索的顺序为我们书写时的顺序,拿上面的例子来说,我们应该先搜索 src 目录下的文件,再搜索 car 目录下的文件。
注意:无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make 就会使用我们当前目录下的文件。
VPATH=src car
test:test.o
gcc -o $@ $^
假设 test.c 文件没有在当前的目录而在当前文件的子目录 “src” 或者是 “car” 下,程序执行是没有问题的,但是生成的 test 的文件没有在定义的子目录文件中而是在当前的目录下,当然生成文件路径可以指定。
vpath的使用
VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。
具体用法:
- vpath PATTERN DIRECTORIES
- vpath PATTERN
- vpath
( PATTERN
:可以理解为要寻找的条件,DIRECTORIES
:寻找的路径 )
首先是用法一,命令格式如下:
vpath test.c src
可以这样理解,在 src 路径下搜索文件 test.c
。多路径的书写规则如下:
vpath test.c src car 或者是 vpath test.c src : car
多路径的用法其实和 VPATH 差不多,都是使用空格或者是冒号分隔开,搜索路径的顺序是先 src 目录,然后是 car 目录。
其次是用法二,命令格式如下:
vpath test.c
用法二的意思是清除符合文件 test.c 的搜索目录。
最后是用法三,命令格式如下:
vpath
vpath 单独使的意思是清除所有已被设置的文件搜索路径。
另外在使用 vpath 的时候,搜索的条件中可以包含模式字符“%”,这个符号的作用是匹配一个或者是多个字符,例如“%.c”表示搜索路径下所有的 .c 结尾的文件。如果搜索条件中没有包含“%" ,那么搜索的文件就是具体的文件名称。
路径示例
我们把源代码放置在src目录下,包含的文件文件是:list1.c、list2.c、main.c 文件,我们把头文件包含在 include 的目录下,包含文件 list1.h、list2.h 文件。Makefile 放在这两个目录文件的上一级目录。
我们按照之前的方式来编写 Makefile 文件:
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<
我们编译执行的 make 时候会发现命令行提示我们:
make:*** No rule to make target 'main.c',need by 'main.o'. stop.
出现错误并且编译停止了,为什么会出现错误呢?我们来看一下出现错误的原因,再去重建最终目标文件 main 的时候我们需要 main.o 文件,但是我们再去重建目标main.o 文件的时候,发现没有找到指定的 main.c 文件,这是错误的根本原因。
这个时候我们就应该添加上路径搜索,我们知道路径搜索的方法有两个:VPATH 和 vpath。我们先来使用一下 VPATH,使用方式很简单,我们只需要在上述的文件开头加上这样一句话:
VPATH=src include
再去执行 make 就不会出现错误。所以 Makefile 中的最终写法是这样的:
VPATH=src include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<
我们使用 vpath 的话同样可以解决这样的问题,只需要把上述代码中的 VPATH 所在行的代码改写成:
vpath %.c src
vpath %.h include
这样我们就可以用 vpath 实现功能,代码的最终展示为:
vpath %.c src
vpath %.h include
main:main.o list1.o list2.o
gcc -o $@ $<
main.o:main.c
gcc -o $@ $^
list1.o:list1.c list1.h
gcc -o $@ $<
list2.o:list2.c list2.h
gcc -o $@ $<
隐含规则
所谓的隐含规则就是需要我们做出具体的操作,系统自动完成。编写 Makefile 的时候,可以使用隐含规则来简化Makefile 文件编写。
test:test.o
gcc -o test test.o
test.o:test.c
我们可以在 Makefile 中这样写来编译 test.c 源文件,相比较之前少写了重建 test.o 的命令。但是执行 make,发现依然重建了 test 和 test.o 文件,运行结果却没有改变。这其实就是隐含规则的作用。在某些时候其实不需要给出重建目标文件的命令,有的甚至可以不需要给出规则。实例:
test:test.o
gcc -o test test.o
运行的结果是相同的。
注意:隐含条件只能省略中间目标文件重建的命令和规则,但是最终目标的命令和规则不能省略。
ifeq、ifneq、ifdef和ifndef(条件判断)
日常使用 Makefile 编译文件时,可能会遇到需要分条件执行的情况,比如在一个工程文件中,可编译的源文件很多,但是它们的类型是不相同的,所以编译文件使用的编译器也是不同的。手动编译去操作文件显然是不可行的(每个文件编译时需要注意的事项很多),所以 make 为我们提供了条件判断来解决这样的问题。
需要解决的问题:要根据判断,分条件执行语句。
条件语句的作用:条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。
条件语句使用优点:Makefile 中使用条件控制可以做到处理的灵活性和高效性。
注意:条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。
下面是条件判断中使用到的一些关键字:
关键字 | 功能 |
ifeq | 判断参数是否不相等,相等为 true,不相等为 false。 |
ifneq | 判断参数是否不相等,不相等为 true,相等为 false。 |
ifdef | 判断是否有值,有值为 true,没有值为 false。 |
ifndef | 判断是否有值,没有值为 true,有值为 false。 |
ifeq 和 ifneq
条件判断的使用方式如下:
ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"
实例:
纯文本复制
libs_for_gcc= -lgnu
normal_libs=
foo:$(objects)
ifeq($(CC),gcc)
$(CC) -o foo $(objects) $(libs_for_gcc)
else
$(CC) -o foo $(objects) $(noemal_libs)
endif
条件语句中使用到三个关键字“ifeq”、“else”、“endif”。其中:“ifeq”表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。“ifeq”,后面的是条件满足的时候执行的,条件不满足忽略;“else”表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;“endif”是判断语句结束标志,Makefile 中条件判断的结束都要有。
其实 “ifneq” 和 “ifeq” 的使用方法是完全相同的,只不过是满足条件后执行的语句正好相反。
上面的例子可以换一种更加简介的方式来写:
纯文本复制
libs_for_gcc= -lgnu
normal_libs=
ifeq($(CC),gcc)
libs=$(libs_for_gcc)
else
libs=$(normal_libs)
endif
foo:$(objects)
$(CC) -o foo $(objects) $(libs)
ifdef 和 ifndef
使用方式如下:
ifdef VARIABLE-NAME
它的主要功能是判断变量的值是不是为空,实例:
bar =
foo = $(bar)
all:
ifdef foo
@echo yes
else
@echo no
endif
foo=
all:
ifdef foo
@echo yes
else
@echo no
endif
通过两个实例对比说明:通过打印 “yes” 或 “no” 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 “yes” ,实例 2打印的结果是 “no” 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。
伪目标
所谓的伪目标可以这样来理解,它并不会创建目标文件,只是想去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。
使用伪目标有两点原因:
- 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
- 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。
我们先来看一下第一种情况的使用。如果需要书写这样一个规则,规则所定义的命令不是去创建文件,而是通过 make 命令明确指定它来执行一些特定的命令。实例:
clean:
rm -rf *.o test
规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean
命令,命令 rm -rf *.o test
总会被执行 ,这也是我们期望的结果。
如果当前目录下存在文件名为 clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。为了解决这个问题,删除 clean 文件或者是在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如下:
.PHONY:clean
这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。
在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 “clean” 的完整书写格式如下:
.PHONY:clean
clean:
rm -rf *.o test
伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:
SUBDIRS=foo bar baz
subdirs:
for dir in $(SUBDIRS);do $(MAKE) -C $$dir;done
代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的 Makefile 文件,代码中实现的部分是用当前目录下的 Makefile 控制其它子模块中的 Makefile 的运行,但是这种实现方法存在以下几个问题:
- 当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行make。在最终执行失败的情况下,我们很难根据错误提示定位出具体实在那个目录下执行 make发生的错误。这样给问题定位造成很大的困难。为了解决问题可以在命令部分加入错误检测,在命令执行的错误后主动退出。不幸的是如果在执行 make 时使用了 “-k” 选项,此方式将失效。
- 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell命令,不能被并行处理。
有了伪目标之后,我们可以用它来克服以上方式所存在的两个问题,代码展示如下:
SUBDIRS=foo bar baz
.PHONY:subdirs $(SUBDIRS)
subdirs:$(SUBDIRS)
$(SUBDIRS):
$(MAKE) -C $@
foo:baz
上面的实例中有一个没有命令行的规则“foo:baz”,这个规则是用来规定三个子目录的编译顺序。因为在规则中 “baz” 的子目录被当作成了 “foo” 的依赖文件,所以 “baz” 要比 “foo” 子目录更先执行,最后执行 “bar” 子目录的编译。
一般情况下,一个伪目标不作为另外一个目标的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则伪目标所定义的命令都会被执行(因为它作为规则的依赖,重建规则目标时需要首先重建规则的所有依赖文件)。当一个伪目标没有任何目标(此目标是一个可被创建或者是已存在的文件)的依赖时,我们只能通过 make 的命令来明确的指定它的终极目标,执行它所在规则所定义的命令。例如 make clean。
伪目标实现多文件编辑
如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。使用方式如下:
.PHONY:all
all:test1 test2 test3
test1:test1.o
gcc -o $@ $^
test2:test2.o
gcc -o $@ $^
test3:test3.o
gcc -o $@ $
常用字符串处理函数
学习使用函数之前,先来看一下函数的语法结构。函数的调用和变量的调用很像。引用变量的格式为$(变量名),函数调用的格式如下:
$(或者是 ${ ) }
其中,function
是函数名,arguments
是函数的参数,参数之间要用逗号分隔开。而参数和函数名之间使用空格分开。调用函数的时候要使用字符“$”,后面可以跟小括号也可以使用花括号。这个其实我们并不陌生,我们之前使用过许多的函数,比如说展开通配符的函数 wildcard
,以及字符串替换的函数 patsubst
,Makefile 中函数并不是很多。
今天主要讲的是字符串处理函数,这些都是我们经常使用到的函数,下面是对函数详细的介绍。
- 模式字符串替换函数,函数使用格式如下:
$(patsubst ,,)
函数说明:函数功能是查找 text
中的单词是否符合模式 pattern
,如果匹配的话,则用 replacement
替换。返回值为替换后的新字符串。实例:
OBJ=$(patsubst %.c,%.o,1.c 2.c 3.c)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是 “1.o 2.o 3.o
”,这些都是替换后的值。
- 字符串替换函数,函数使用格式如下:
$(subst ,,)
函数说明:函数的功能是把字符串中的 form 替换成 to,返回值为替换后的新字符串。实例:
OBJ=$(subst ee,EE,feet on the street)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“fEEt on the strEEt
”。
- 去空格函数,函数使用格式如下:
$(strip )
函数说明:函数的功能是去掉字符串的开头和结尾的字符串,并且将其中的多个连续的空格合并成为一个空格。返回值为去掉空格后的字符串。实例:
OBJ=$(strip a b c)
all:
@echo $(OBJ)
执行完 make 之后,结果是“a b c
”。这个只是除去开头和结尾的空格字符,并且将字符串中的空格合并成为一个空格。
- 查找字符串函数,函数使用格式如下:
$(findstring ,)
函数说明:函数的功能是查找 in 中的 find ,如果我们查找的目标字符串存在。返回值为目标字符串,如果不存在就返回空。实例:
OBJ=$(findstring a,a b c)
all:
@echo $(OBJ)
执行 make 命令,得到的返回的结果就是 “a”。
- 过滤函数,函数使用格式如下:
$(filter ,)
函数说明:函数的功能是过滤出 text 中符合模式 pattern 的字符串,可以有多个 pattern 。返回值为过滤后的字符串。实例:
OBJ=$(filter %.c %.o,1.c 2.o 3.s)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“1.c 2.o”。
- 反过滤函数,函数使用格式如下:
$(filter-out ,)
函数说明:函数的功能是功能和 filter 函数正好相反,但是用法相同。去除符合模式 pattern 的字符串,保留符合的字符串。返回值是保留的字符串。实例:
OBJ=$(filter-out 1.c 2.o ,1.o 2.c 3.s)
all:
@echo $(OBJ)
执行 make 命令,打印的结果是“3.s”。
- 排序函数,函数使用格式如下:
$(sort )
函数说明:函数的功能是将 中的单词排序(升序)。返回值为排列后的字符串。实例:
OBJ=$(sort foo bar foo lost)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“bar foo lost”。
注意:sort会去除重复的字符串。
8. 取单词函数,函数使用格式如下:
$(word ,)
函数说明:函数的功能是取出函数 中的第n个单词。返回值为我们取出的第 n 个单词。实例:
OBJ=$(word 2,1.c 2.c 3.c)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“2.c”。
常用文件名操作函数
我们在编写 Makefile 的时候,很多情况下需要对文件名进行操作。例如获取文件的路径,去除文件的路径,取出文件前缀或后缀等等。当遇到这样的问题的时手动修改是不太可能的,因为文件可能会很多,而且 Makefile 中操作文件名可能不止一次。所以 Makefile 给我们提供了相应的函数去实现文件名的操作。
注意:下面的每个函数的参数字符串都会被当作或是一个系列的文件名来看待。
- 取目录函数,函数使用格式如下:
$(dir )
函数说明:函数的功能是从文件名序列 names 中取出目录部分,如果没有 names 中没有 “/” ,取出的值为 “./” 。返回值为目录部分,指的是最后一个反斜杠之前的部分。如果没有反斜杠将返回“./”。实例:
OBJ=$(dir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“src/ ./
”。提取文件 foo.c
的路径是 “/src
” 和文件 hacks
的路径 “./”。
- 取文件函数,函数使用格式如下:
$(notdir )
函数说明:函数的功能是从文件名序列 names
中取出非目录的部分。非目录的部分是最后一个反斜杠之后的部分。返回值为文件非目录的部分。实例:
OBJ=$(notdir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“foo.c hacks
”。
- 取后缀名函数,函数使用格式如下:
$(suffix )
函数说明:函数的功能是从文件名序列中 names 中取出各个文件的后缀名。返回值为文件名序列 names 中的后缀序列,如果文件没有后缀名,则返回空字符串。实例:
OBJ=$(suffix src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们得到的值是“.c ”。文件 “hacks” 没有后缀名,所以返回的是空值。
- 取前缀函数,函数使用格式如下:
$(basename )
函数说明:函数的功能是从文件名序列 names 中取出各个文件名的前缀部分。返回值为被取出来的文件的前缀名,如果文件没有前缀名则返回空的字符串。实例:
OBJ=$(notdir src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到值是“src/foo hacks
”。获取的是文件的前缀名,包含文件路径的部分。
- 添加后缀名函数,函数使用格式如下:
$(addsuffix ,)
函数说明:函数的功能是把后缀 suffix 加到 names 中的每个单词后面。返回值为添加上后缀的文件名序列。实例:
OBJ=$(addsuffix .c,src/foo.c hacks)
all:
@echo $(OBJ)
执行 make 后我们可以得到“sec/foo.c.c hack.c
”。我们可以看到如果文件名存在后缀名,依然会加上。
- 添加前缀名函数,函数使用格式如下:
$(addperfix ,)
函数说明:函数的功能是把前缀 prefix 加到 names 中的每个单词的前面。返回值为添加上前缀的文件名序列。实例:
OBJ=$(addprefix src/, foo.c hacks)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到值是 “src/foo.c src/hacks
” 。我们可以使用这个函数给我们的文件添加路径。
- 链接函数,函数使用格式如下:
$(join ,)
函数说明:函数功能是把 list2 中的单词对应的拼接到 list1 的后面。如果 list1 的单词要比 list2的多,那么,list1 中多出来的单词将保持原样,如果 list1 中的单词要比 list2 中的单词少,那么 list2 中多出来的单词将保持原样。返回值为拼接好的字符串。实例:
OBJ=$(join src car,abc zxc qwe)
all:
@echo $(OBJ)
执行 make 命令,我们可以得到的值是“srcabc carzxc qwe
”。很显然<list1>
中的文件名比<list2>
的少,所以多出来的保持不变。
- 获取匹配模式文件名函数,命令使用格式如下:
$(wildcard PATTERN)
函数说明:函数的功能是列出当前目录下所有符合模式的 PATTERN 格式的文件名。返回值为空格分隔并且存在当前目录下的所有符合模式 PATTERN 的文件名。实例:
OBJ=$(wildcard *.c *.h)
all:
@echo $(OBJ)
返回:main.c echo.h
执行 make 命令,可以得到当前函数下所有的 ".c " 和 “.h” 结尾的文件。这个函数通常跟的通配符 “*” 连用,使用在依赖规则的描述的时候被展开(在这里我们的例子如果没有 wildcard 函数,我们的运行结果也是这样,“echo” 属于 shell 命令,在使用通配符的时通配符自动展开,我们这里只是相要说明一下这个函数在使用时,如果通过引用变量出现在规则中要被使用)。
其它常用函数
之前学习过了 Makefile 中的字符串操作文件和文件名操作函数,我们今天再来看一下 Makefile 中的其他的函数。以下是这些函数的详细说明。
foreach
$(foreach ,,)
函数的功能是:把参数<list>
中的单词逐一取出放到参数<var>
所指定的变量中,然后再执行<text>
所包含的表达式。每一次<text>
会返回一个字符串,循环过程中,<text>
的返所返回的每个字符串会以空格分割,最后当整个循环结束的时候,<text>
所返回的每个字符串所组成的整个字符串(以空格分隔)将会是 foreach
函数的返回值。所以<var>
最好是一个变量名,<list>
可以是一个表达式,而<text>
中一般会只用<var>
这个参数来一次枚举<list>
中的单词。
name:=a b c d
files:=$(foreach n,$(names),$(n).o)
all:
@echo $(files)
执行 make 命令,我们得到的值是“a.o b.o c.o d.o
”。
注意,foreach 中的 参数是一个临时的局部变量,foreach 函数执行完后,参数的变量将不再作用,其作用域只在 foreach 函数当中。
if
$(if, 或(if) , , )
可见,if 函数可以包含else部分,或者是不包含,即if函数的参数可以是两个,也可以是三个。condition
参数是 if 表达式,如果其返回的是非空的字符串,那么这个表达式就相当于返回真,于是,then-part
就会被计算,否则else-part
会被计算。
而if函数的返回值是:如果condition为真(非空字符串),那么then-part会是整个函数的返回值。如果condition为假(空字符串),那么else-part将会是这个函数的返回值。此时如果else-part没有被定义,那么整个函数返回空字串符。所以,then-part和else-part只会有一个被计算。
OBJ:=foo.c
OBJ:=$(if $(OBJ),$(OBJ),main.c)
all:
@echo $(OBJ)
执行 make 命令我们可以得到函数的值是 foo.c
,如果变量 OBJ 的值为空的话,我们得到的 OBJ 的值就是main.c
。
call
$(call ,,,,...)
call 函数是唯一一个可以用来创建新的参数化的函数。我们可以用来写一个非常复杂的表达式,这个表达式中,我们可以定义很多的参数,然后你可以用 call 函数来向这个表达式传递参数。
当 make 执行这个函数的时候,expressio
n参数中的变量$(1)、$(2)、$(3)
等,会被参数parm1,parm2,parm3依次取代。而expression的返回值就是 call 函数的返回值。
实例 1:
reverse = $(1) $(2)
foo = $(call reverse,a,b)
all:
@echo $(foo)
那么,foo 的值就是“a b
”。当然,参数的次序可以是自定义的,不一定是顺序的,
实例 2:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
all:
@echo $(foo)
此时的 foo 的值就是“b a
”
origin
$(origin )
origin
函数不像其他的函数,它并不操作变量的值,它只是告诉你这个变量是哪里来的。
注意: variable
是变量的名字,不应该是引用,所以最好不要在 variable 中使用“$”字符。origin 函数会员其返回值来告诉你这个变量的“出生情况”。
下面是origin函数返回值:
- “
undefined
”:如果<variable>
从来没有定义过,函数将返回这个值。 - “
default
”:如果<variable>
是一个默认的定义,比如说“CC”这个变量。 - “
environment
”:如果<variable>
是一个环境变量并且当Makefile被执行的时候,“-e”参数没有被打开。 - “
file
”:如果<variable>
这个变量被定义在Makefile中,将会返回这个值。 - “
command line
”:如果<variable>
这个变量是被命令执行的,将会被返回。 - “
override
”:如果<variable>
是被override指示符重新定义的。 - “
automatic
”:如果<variable>
是一个命令运行中的自动化变量。
这些信息对于我们编写 Makefile 是非常有用的,例如假设我们有一个 Makefile ,其包含了一个定义文件Make.def
,在Make.def
中定义了一个变量bletch
,而我们的环境变量中也有一个环境变量bletch
,我们想去判断一下这个变量是不是环境变量,如果是我们就把它重定义了。如果是非环境变量,那么我们就不重新定义它。于是,我们在 Makefile 中,可以这样写:
ifdef bletch
ifeq "$(origin bletch)" "environment"
bletch = barf,gag,etc
endif
endif
当然,使用override关键字不就可以重新定义环境中的变量了吗,为什么需要使用这样的步骤?是的,我们用override是可以达到这样的效果的,可是override会把从命令行定义的变量也覆盖了,而我们只想重新定义环境传来的,而不是重新定义命令行传来的。
shell pwd
make_dir:=$(shell pwd) // := 的意思是将$(shell pwd) 表示为make_dir,类似于宏定义
make_dir2:=$(pwd)
all:
@echo $(make_dir) //有输出
@echo $(make_dir2) //无输出
返回
/root/hello //有输出
//无输出
lastword
lastword
这个函数表示提取最后一个MAKEFILE_LIST
列表里的最后一个元素。元素与元素之间是以空格符分开。 $(lastword $(MAKEFILE_LIST))
表示提取最后一个Makefile
parent_dir_fname:=$(shell pwd)/$(lastword $(MAKEFILE_LIST))
all:
@echo $(parent_dir_fname)
返回
/root/hello/Makefile
命令的编写
我们已经知道了 Makefile 的规则是什么,他是由依赖关系规则和命令组成的。所使用的命令是由 shell 命令行组成,他们是一条一条执行的。多个命令之间要使用分号隔开,Makefile 中的任何命令都要以tab键开始。多个命令行之间可以有空行和注释行,在执行规则时空行会被自动忽略。
通常系统中可能存在不同的 shell 。但是 make 处理 Makefile 过程时,如果没有明确的指定,那么对所有规则中的命令行的解析使用bin/sh来完成。执行过程中使用的 shell 决定了规则中的命令的语法和处理机制。当使用默认的bin/sh时,命令中出现的字符“#”到行末的内容被认为是注释。当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注释处理。
命令回显
通常 make 在执行命令行之前会把要是执行的命令行输出到标准输出设备。我们称之为 “回显”,就好像我们在 shell 环境下输入命令执行时一样。如果规则的命令行以字符“@”开始,则 make 在执行的时候就不会显示这个将要被执行的命令。典型的用法是在使用echo命令输出一些信息时。
OBJ=test main list
all:
@echo $(OBJ)
执行时将会得到test main list这条输出信息,如果在执行命令之前没有字符“@”,那么make的输出将是echo test main list
我们在执行 make 时添加上一些参数,可以控制命令行是否输出。当使用 make 的时候机加上参数-n或者是–just-print ,执行时只显示所要执行的命令,但不会真正的执行这个命令。只有在这种情况下 make 才会打印出所有的 make 需要执行的命令,其中包括了使用的“@”字符开始的命令。这个选项对于我们调试 Makefile 非常的有用,使用这个选项就可以按执行顺序打印出 Makefile 中所需要执行的所有命令。而 make 参数-s或者是–slient则是禁止所有的执行命令的显示。就好像所有的命令行都使用“@”开始一样。
make -n
只是输出,不执行
命令的执行
当规则中的目标需要被重建的时候,此规则所定义的命令将会被执行,如果是多行的命令,那么每一行命令将是在一个独立的子 shell 进程中被执行。因此,多命令行之间的执行命令时是相互独立的,相互之间不存在以来。
在 Makefile 中书写在同一行中的多个命令属于一个完整的 shell 命令行,书写在独立行的一条命令是一个独立的 shell 命令行。因此:在一个规则的命令中命令行 “cd”改变目录不会对其后面的命令的执行产生影响。就是说之后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。如果达到这个目的,就不能把“cd”和其后面的命令放在两行来书写。而应该把这两个命令放在一行上用分号隔开。这样才是一个完整的 shell 命令行。
foo:bar/lose
cd bar;gobble lose >../foo
如果想把一个完整的shell命令行书写在多行上,需要使用反斜杠 ()来对处于多行的命令进行连接,表示他们是一个完整的shell命令行。例如上例我们也可以这样书写:
foo:bar.lose
cd bar; \
gobble lose > ../foo
并发执行命令
GNU make 支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令结束之后才能够开始执行。不过可以通过 make 命令行选项 “-j” 或者 “–jobs” 来告诉 make 在同一时刻可以允许多条命令同时执行。
如果选项 “-j” 之后存在一个整数,其含义是告诉 make 在同一时刻可以允许同时执行的命令行的数目。这个数字被称为job slots。当 “-j” 选项中没有出现数字的时候,那么同一时间执行的命令数目没有要求。使用默认的job solts,值为1,表示make将串行的执行规则的命令(同一时刻只能由一条命令被执行)。
并行执行命令所带来的问题是显而易见的:
- 多个同时执行的命令的输出信息将同时被输出到终端。当出现错误时很难根据一大堆凌乱的信息来区分那条命令执行错误。
- 在同一时刻可能会存在多个命令执行的进程同时读取到标准输入,但是对于白哦准输入设备来说,在同一时刻只能存在一个进程访问它。就是说在某个时间点,make只能保证此刻正在执行的进程中的一个进程读取标准输入流。而其他的进程键的标准输入流将设置为无效。因此在此一时刻多个执行命令的进程中只有一个进程获得标准输入,而其他的需要读取标准输入流的进程由于输入流无效而导致致命的错误。