一、通配符
“*”:匹配任意数量的字符,包含0个和多个字符
“*.c”:匹配所有以".c"结尾的文件(包括空字符串)
“%.c”:匹配所有以".c"结尾的文件(不包括空字符串)
“?”:匹配单个任意字符
“doc?.txt”:可以匹配"doc1.txt"、"docA.txt"等文件,但无法匹配"doc10.txt"等非单个字符文件
“[characters]”:匹配方括号中任意一个字符
“[ab]*.c”:匹配所有以"a"或"b"开头,且以.c结尾的文件名
“*.[cho]”:匹配所有以"c"、“h”、"o"后缀的文件名
“[^characters]”:匹配除了方括号中指定的字符以外的任意一个字符
注意,makefile中使用通配符时,通配符需要用引号括起来避免被shell扩展成为实际匹配的文件列表,导致执行出错
正常情况下,假设当前文件夹下有三个文件"foo.c"、“bar.c”、“baz.h”
现在执行命令
gcc *.c -o myapp
在执行这个命令前,shell会将通配符"*.c"扩展成文件列表"foo.c"和"bar.c",再将扩展后的命令行参数传给gcc编译器,即等价于
gcc foo.c bar.c -o myapp
这种行为就是通配符被shell扩展成实际匹配的文件列表
特殊情况下
foo:*.c
gcc *.c -o foo
在这个规则里,当文件目录下只存在一个".c"文件时规则可以正常执行,如果存在多个".c"文件就会执行失败,gcc命令会以"foo.c bar.c"的顺序传入参数,最终编译成目标二进制文件"foo",但因为gcc编译器必须接受一堆具体的文件名作为参数而不是通配符,
所以会导致编译错误,因此需要用引号将通配符括起来避免这种扩展行为,如下方示例
foo:*.c
gcc "$(wildcard *.c)" -o foo
这样shell就无法对其进行扩展,"$(wildcard *.c)"会返回满足模式的具体文件名列表
或
SRCS=$(wildcard *.c)
foo:$(SRCS)
gcc $^ -o $@
在这上述规则中,"$(wildcard *.c)“会获取所有的”.c"文件列表名,并存入变量"SRCS"中,按字母顺序排序。在"foo"规则中,使用
“$(SRCS)“代替”*.c"的方式,可以避免重复定义目标文件的问题。在实际编译过程中,gcc命令会先将”.c"文件进行编译,然后再进行链接生成目标文件
“*.c"与”$(wildcard *.c)"
“*.c"匹配指定目录下(或是当前目录下)所有以”.c"结尾的文件,文件名是固定的,并且顺序是不确定的,直接使用会导致重复定义目标文件从而造成编译失败
“$(wildcard *.c)“表示获取当前文件夹下所有以”.c"后缀的文件,保存在一个变量中,这个变量包含了所有”.c"文件名,并按字母顺序排序。使用会将它们作为完整的目标文件名进行编译,每个".c"文件被视为一个独立的目标文件进行编译,不会导致重复定义目标文件的问题
二、“VPATH"与"vpath”
大型工程中各模块功能代码会存放在不同文件夹中,当make在当前目录找不到文件时候,会根据变量"VPATH"所指定的目录去寻找文件
1、环境变量"YPATH"
例如,将一个C文件与一个位于"/usr/include"目录下的头文件一起编译↓
gcc -o main main.c -l/usr/include
但在许多情况下,涉及到的多个源文件可能需要访问多个位置的头文件,路径远不止一个,在这种情况下,路径都写在命令里就很麻烦且容易出错,为了解决这个问题,可以通过设置环境变量"VPATH"来指定头文件路径↓
YPATH:=/usr/include
在编译时,可以使用"-l$(YPATH)"选项来将"YPATH"指定的路径包含在文件搜索路径中↓
gcc -o main main.c -l$(YPATH)
多个路径的写法,在中间用冒号分割↓
YPATH:= /usr/include:/opt/local/include:../headers
查找顺序为当前目录,其次按从左到右顺序查找
特殊情况下,路径中也会包含":",这种情况下可以用空格或分号来表示分割,如Windows系统中的路径↓
YPATH:=C:\Program Files\include D:\lib\include
同时要注意,在makefile中使用路径变量时,必须将路径中的空格或冒号用双引号括起来,否则可能导致语法错误和路径解析错误↓
YPATH:="/usr/include" "/opt/local/include"
或
YPATH:="C:\Program Files\include" "D:\lib\include"
2、关键字"vpath"
另一个设置文件搜索路径的方法是使用make的"vpath"关键字
(注意:一个为变量,一个为关键字,且关键字为全小写)
与VPATH变量类似,但更为灵活,可以指定不同的文件在不同的搜索目录中
vpath<pattern><dirs>
<pattern>:表示匹配的文件模式,可以使用通配符来匹配多个文件,例如"%h"表示所有的头文件
<dirs>:表示包含匹配文件的目录路径,可以包含多个路径,以空格分隔
vpath %.h include
vpath %.c src
vpath %.cpp src
上述写法中,“vpath %.h include"将所有的”.h"文件的搜索路径设置为"include"目录,“vpath %.c src"将所有的”.c"文件的搜索路径设置为"src"目录。
在编译时就会使用"#include"指定,编译器会在"include"目录搜索头文件,在"src"目录搜索源文件
注意:如果定义多个目录,同一个文件在一个或多个目录中存在,则以第一个搜索到的文件作为匹配
三、伪目标".PHONY"
之前提到过例子
.PHONY : clean
clean :
rm *.o
其中"clean"是一个伪目标,伪目标并不用来生成文件,而是作为一个标签,make不会去主动执行,只有使用"make clean"来执行
all: abc def ghi
.PHONY: all
abc:abc.o
cc -o abc abc.o
在上述例子中,"all"放在了第一个作为默认目标,但因为all是伪目标,所以会按需执行任务,但不会生成"all"文件
.PHONY:cleanall cleanobj cleantxt
cleanall:cleanobj cleantxt
rm program
cleanobj:
rm *.o
cleantxt:
rm *.txt
在上述例子中,使用"make cleanall"可以清除所有文件
"make cleanobj"和"make cleantxt"则可以分别清除对应的后缀文件
vpath<pattern>
vpath
利用这俩种语法可以清除之前定义的搜索目录路径,前者清除指定文件的搜索目录,后者清除所有设置好的目录包括变量"VPATH"设置的路径
四、多目标
有时多个目标依赖于一个文件,并且其生成命令大致相同,就可以将其合并起来同时生成,从而提高代码编译或构件的效率
多目标功能通常在代码库或项目规模较大的情况下使用,其中不同的目标包含了各种操作,例如编译、链接、静态分析、测试和发布等
优点:
1、提高效率:使用多目标功能可以将不同操作放在一个Makefile文件中,使得每次构建偶编译时可以一次性完成多个目标的生成,
从而提高效率
2、灵活性:多目标功能使得Makefile文件更加灵活,可以根据不同的需求编写不同的目标,从而实现各种操作,例如编译、链接、
测试和发布等,使得整个代码构建过程更加自由
3、依赖关系清晰:使用多目标功能可以清晰的定义不同目标之间的依赖关系,从而确保代码构建的正确性和稳定性
缺点:
1、生成的目标增加了复杂度:Makefile文件中包含多个目标,会使得代码库的Makefile文件变得复杂,如果代码库较大,可能需要花费更多的时间和经历来维护Makefile文件
2、可读性降低:Makefile文件中包含多个目标,代码的可读性可能会降低,需要通过精细的注释和结构来保证Makefile的可读性
3、维护成本增加:随着不同目标的增加、Makefile文件的维护成本可能会逐渐增加,需要花费更多的精力来保证Makefile文件的正确性和可靠性
总结:
更效率具有灵活性,但降低了可读性并增加维护成本
示例:
foo:foo.c
gcc -o $@ $<
bar:bar.c
gcc -o $@ $<
上述例子中,定义了两个目标"foo"和"bar",默认情况下执行make,只会默认第一个任务也就是生成"foo"可执行文件,或者使用"make foo"或"make bar",也可以同时生成"make foo bar"
.all:foo bar
foo:foo.c
gcc -o $@ $<
bar:bar.c
gcc -o $@ $<
或者按这种写法,使用make就能同时生成"foo"和"bar"两个可执行文件(目标"all"前加上".all"表示指定默认的目标,如果不加就默认第一个)
上述案例中的
@
和
@和
@和<是伪变量或者说是自动化变量,属于Makefile中的内置变量,用来构建编译命令
$@:代表当前目标集合,即规则中":“左侧的所有目标(上述案例中代表foo或bar)
$<:代表当前依赖项列表的第一个依赖项,即规则中”:"右侧第一个(上述中代表foo.c或bar.c)
五、静态模式
Makefile的静态模式是一种特殊的规则写法方式,在规则中使用变量来指定目标文件和依赖文件。
相比于普通规则,静态模式可以减少Makefile中繁长的代码,使得Makefile更加简介易读,适用于一些重复性的操作。
targets:target-pattern:prerequisite-patten
commands
targets:表示当前规则的目标列表,可以是一个或多个目标
target-pattern:表示targets的模式,目标集模式,也就是当前规则中目标文件的通配符模板,必须包含一个"%"符合
prereq-patterns:目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义,当中的"%"符号个数相同,并按顺序一一对应
这个语法就像是定义了一组模板,用于在Makefile中声明相同规则的多个目标,使用静态模式时,变量"$@“会被自动替换成当前的目标,变量”$<"会被替换成当前文件的第一个依赖文件
objects=main.o func1.o func2.o
main.o:main.c
gcc -c -o main.o main.c
func1.o:func1.c
gcc -c -o func1.o func1.c
func2.o:func2.c
gcc -c -o func2.o func2.c
program:$(objects)
gcc -o program $(objects)
objects=main.o func1.o func2.o
%o:%c
gcc -c -o $@ $<
program:$(objects)
gcc -o $@ $(objects)
上述对比例子中(前者未使用静态模式,后者使用了静态模式),定义了一个变量"objects",包含三个目标文件"main.o"、“func1.o”、“func2.o"的名称,然后使用了一个静态模式规则,通过通配符”%.o"来匹配所有目标文件的模板,再使用约定好的规则来编译目标文件。最后定义了一个"program"目标,其依赖于所有的目标文件,生成可执行文件"program"。
六、自动生成依赖关系
在Makefile中,自动生成依赖关系可以通过"-M"或"-MM"参数实现,二者区别在于输出依赖关系时候是否包含了系统头文件的依赖
(“-M"包含系统头文件;”-MM"不包含系统头文件)
//main.c
#include <stdio.h>
#include "header.h"
int main(void){
printf("hello world\n");
printf_header();
return 0;
}
调用"gcc -M main.c"输出的依赖关系为
main.o: main.c header.h /usr/include/stdio.h /usr/include/features.h \
/usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
/usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
/usr/include/bits/sched.h /usr/include/libio.h \
/usr/include/_G_config.h /usr/include/wchar.h \
/usr/include/bits/wchar.h /usr/include/gconv.h \
/usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
/usr/include/bits/stdio_lim.h
调用"gcc -MM main.c"输出的依赖关系描述为
main.o:main.c header.h
可以发现后者忽略了系统头文件的依赖,因此"-MM"比"-M"参数更适合在Makefile中使用,可以避免系统头文件的变化对Makefile的重新生成造成影响
输入语句"gcc -MM main.c"后进行以下操作
1、预处理源文件"main.c",并输出预处理结果
2、在预处理结果中查找头文件包含语句"#include",并输出每个头文件对应的依赖关系到标准输出
3、不包括系统头文件的依赖关系会输出到标准输出(-M会包括)
生成的依赖关系通常以"Makefile"的格式输出,可以使用输出写入到一个"Makefile"中保存
在Makefile中,每个源文件需要编译成目标文件,并且该源文件所使用的的头文件都需要列在其依赖关系所述的目标文件后
#使用gcc -MM main.c语句将依赖关系写入Makefile
main.o:main.c header.h
gcc -c main.c