自学了一下GNU make工具中Makefile文件的基本编写规则,还查了一些其他资料,把觉得重要的记录下来了。
Makefile教程
Makefile 文件描述了 Linux 系统下 C/C++ 工程的编译规则,它用来自动化编译 C/C++ 项目。一旦写编写好 Makefile 文件,只需要一个 make 命令,整个工程就开始自动编译,不再需要手动执行 GCC 命令。
一个中大型 C/C++ 工程的源文件有成百上千个,它们按照功能、模块、类型分别放在不同的目录中,Makefile 文件定义了一系列规则,指明了源文件的编译顺序、依赖关系、是否需要重新编译等。
在 Linux(unix )环境下使用GNU 的make工具能够比较容易的构建一个属于你自己的工程,整个工程的编译只需要一个命令就可以完成编译、连接以至于最后的执行。
Makefile 可以简单的认为是一个工程文件的编译规则,描述了整个工程的编译和链接等规则。其中包含了那些文件需要编译,那些文件不需要编译,那些文件需要先编译,那些文件需要后编译,那些文件需要重建等等。编译整个工程需要涉及到的,在 Makefile 中都可以进行描述。换句话说,Makefile 可以使得我们的项目工程的编译变得自动化,不需要每次都手动输入一堆源文件和参数。
Makefile 支持多线程并发操作,会极大的缩短我们的编译时间,并且当我们修改了源文件之后,编译整个工程的时候,make 命令只会编译我们修改过的文件,没有修改的文件不用重新编译,也极大的解决了我们耗费时间的问题。
它的规则主要是两个部分组成,分别是依赖的关系和执行的命令:
目标文件:依赖文件
make需要执行的命令(任意的shell命令)。可以有多条命令,每一条命令占一行。
注意:目标和依赖文件之间要使用冒号分隔开,命令的开始一定要使用Tab
键。
例子:
test:test.c #依赖文件:目标文件
gcc -o test test.c #shell命令
使用 Makefile 的方式:首先需要编写好 Makefile 文件,然后在 shell 中执行 make 命令,程序就会自动执行,得到最终的目标文件。
make
当我们在执行 make 条命令的时候,make 就会去当前文件下找要执行的编译规则,也就是 Makefile 文件。
main:main.o
gcc main.o -o main
main.o:main.s
gcc -c main.s -o main.o
main.s:main.i
gcc -S main.i -o main.s
main.i:main.c
gcc -E main.c -o main.i
它的具体工作顺序是:当在 shell 提示符下输入 make 命令以后。 make 读取当前目录下的 Makefile 文件,并将 Makefile 文件中的第一个目标作为其执行的“终极目标”,开始处理第一个规则(终极目标所在的规则)。在我们的例子中,第一个规则就是目标 “main” 所在的规则。规则描述了 “main” 的依赖关系,并定义了链接 “.o” 文件生成目标 “main” 的命令;make 在执行这个规则所定义的命令之前,首先处理目标 “main” 的所有的依赖文件(例子中的 “.o” 文件)的更新规则(以这些 “.o” 文件为目标的规则)。
对这些 “.o” 文件为目标的规则处理有下列三种情况:
目标 “.o” 文件不存在,使用其描述规则创建它;
目标 “.o” 文件存在,目标 “.o” 文件所依赖的 文件中的任何一个比目标 “.o” 文件“更新”(在上一次 make 之后被修改)。则根据规则重新编译生成该目标文件;
目标 “.o” 文件存在,目标 “.o” 文件比它的任何一个依赖文件“更新”(它的依赖文件在上一次 make 之后没有被修改),则什么也不做。
通过上面的更新规则我们可以了解到中间文件的作用,也就是编译时生成的 “.o” 文件。作用是检查某个源文件是不是进行过修改,最终目标文件是不是需要重建。我们执行 make 命令时,只有修改过的源文件或者是不存在的目标文件会进行重建,而那些没有改变的文件不用重新编译,这样在很大程度上节省时间,提高编程效率。小的工程项目可能体会不到,项目工程文件越大,效果才越明显。
Makefile 是可以使用 shell 命令的,所以 shell 支持的通配符在 Makefile 中也是同样适用的。 shell 中使用的通配符有:"*","?","[…]"
通配符 | 说明 |
---|---|
* | 匹配0个或者是任意个字符 |
? | 匹配任意一个字符 |
[] | 可以指定匹配的字符放在 “[]” 中 |
例子:
.PHONY:clean
clean:
rm -rf *.o test#删除任意以 .o 结尾的文件。
还可以使用 % ,它和通配符 “*” 相类似,也是匹配任意个字符。
test:test.o test1.o
gcc -o $@ $^
%.o:%.c
gcc -o $@ $^
“%.o” 把我们需要的所有的 “.o” 文件组合成为一个列表,从列表中挨个取出的每一个文件,"%" 表示取出来文件的文件名(不包含后缀),然后找到文件中和 "%"名称相同的 “.c” 文件,然后执行下面的命令,直到列表中的文件全部被取出来为止。
这个属于 Makefile 中静态模规则:规则存在多个目标,并且不同的目标可以根据目标文件的名字来自动构造出依赖文件。
%通配符和*通配符的区别:
*是应用在系统中的,%是应用在这个Makefile文件中的。
Makefile的通配符%是在带着目的(如“寻找test1.o”)的时候才会把他要寻找的目标套用通配符%中。
而通配符*的意思是遍历目录的文件,看是否匹配。找出所有匹配的文件。
变量的名称=值列表
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)
延迟展开赋值 =
x = $(y)
y = hello
此时x的值是hello。使用等号赋值的时候makefile在分析完整个文件后再把变量展开。
直接展开赋值 :=
:=与我们实际上更符合我们平时写代码的思维,也就是顺序声明变量。使用这种赋值方式,前面的变量不能使用后面的变量,只能使用前面已定义好了的变量,makefile在读到这一行的时候就去确定这个变量的值而不是等全部分析完后把变量展开。
x := hello
y := $(x) world
此时y值为hello world
x := $(y) world
y := hello
此时x的值就不是hello world了,而是world,因为在x变量定义时y还没有定义,因此y还是个空变量,即什么都没有。
条件赋值 ?=
如果变量没有被定义过,那么变量的值就是右侧的值,如果变量先前被定义过,那么这条语句将什么也不做
x ?= hello
追加赋值 +=
在变量后面追加值
x = hello
x += world
x变为hello world
自动化变量的取值根据执行的规则来决定,取决于执行规则的目标文件和依赖文件。
自动化变量 | 描述 |
---|---|
$@ | 目标文件的集合。 |
$^ | 依赖文件的集合。 |
$< | 第一个依赖的文件名 |
$? | 所有比目标文件更新的依赖文件的集合 |
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 $@ $<
常见的搜索的方法的主要有两种:一般搜索VPATH
和选择搜索vpath
。
VPATH 和 vpath 的区别:VPATH 是变量,更具体的说是环境变量,Makefile 中的一种特殊变量,使用时需要指定文件的路径
vpath 是关键字,按照模式搜索,也可以说成是选择搜索。搜索的时候不仅需要加上文件的路径,还需要加上相应限制的条件。
VPATH := src
把 src 的值赋值给变量 VPATH,所以在执行 make 的时候会从 src 目录下找我们需要的文件。
当存在多个路径的时候我们可以这样写:
VPATH := src car
或者
VPATH := src:car
多个路径之间要使用空格或者是冒号隔开,表示在多个路径下搜索文件。搜索的顺序为我们书写时的顺序,拿上面的例子来说,我们应该先搜索 src 目录下的文件,再搜索 car 目录下的文件。
无论你定义了多少路径,make 执行的时候会先搜索当前路径下的文件,当前目录下没有我们要找的文件,才去 VPATH 的路径中去寻找。如果当前目录下有我们要使用的文件,那么 make 就会使用我们当前目录下的文件。
这种搜索方式一般被称作选择性搜索。使用上的区别我们可以这样理解:**VPATH 是搜索路径下所有的文件,而 vpath 更像是添加了限制条件,会过滤出一部分再去寻找。**其格式为:
vpath 搜索的条件 搜索的路径
vpath 搜索的条件
vpath
例子一:
vpath test.c src#在 src 路径下搜索文件 test.c
多路径搜索文件
vpath test.c src car
vpath test.c src : car
例子二:
vpath test.c#清除符合文件 test.c 的搜索目录
例子三:
vpath#清除所有已被设置的文件搜索路径
把源代码放置在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 #与.c匹配的文件到src目录中寻找
vpath %.h include #与.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 $@ $<
关键字 | 功能 |
---|---|
ifeq | 判断参数是否不相等,相等为 true,不相等为 false。 |
ifneq | 判断参数是否不相等,不相等为 true,相等为 false。 |
ifdef | 判断是否有值,有值为 true,没有值为 false。 |
ifndef | 判断是否有值,没有值为 true,有值为 false。 |
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
主要功能是判断变量的值是不是空。ifdef当变量非空时返回true,否则返回false。
#打印yes
bar =
foo = $(bar)
all:
ifdef foo
@echo yes
else
@echo no
endif
#打印no
foo=
all:
ifdef foo
@echo yes
else
@echo no
endif
所谓伪目标就是这样一个目标,它不代表一个真正的文件名,在执行make时可以指定这个目标来执行其所在规则定义的命令,有时我们将一个伪目标称为标签。
它并不会创建目标文件,只是去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。
clean:
rm -rf *.o test
在工作目录下不存在“clean”这个文件时,我们输入“make clean”后,“rm -rf *.o test”总会被执行。这是我们的初衷。
但当前工作目录下存在文件“clean”时情况就不一样了,在我们输入“make clean”时。规则没有依赖文件,所以目标被认为是最新的而不去执行规则作定义的命令,命令“rm”将不会被执行。这并不是我们的初衷。为了避免这个问题,我们可以将目标“clean”明确的声明为伪目标。
在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 “clean” 的完整书写格式如下:
.PHONY:clean
clean:
rm -rf *.o test
这样 clean 就被声明成一个伪目标,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 会被执行,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。
通常 make 在执行命令行之前会把要是执行的命令行输出到标准输出设备。我们称之为 回显,就好像我们在 shell 环境下输入命令执行时一样。如果规则的命令行以字符**@**开始,则 make 在执行的时候就不会显示这个将要被执行的命令。
OBJ=test main list
all:
@echo $(OBJ)
执行时将会得到test main list
这条输出信息,如果在执行命令之前没有字符“@”,那么make的输出将是echo test main list
。
main:main.c
@gcc -o main main.c
执行时什么都不显示。如果没加"@"字符,则会在终端显示gcc -o main main.c
当规则中的目标需要被重建的时候,此规则所定义的命令将会被执行,如果是多行的命令,那么每一行命令将是在一个独立的子 shell 进程中被执行。因此,多命令行之间的执行命令时是相互独立的。
在 Makefile 中书写在同一行中的多个命令属于一个完整的 shell 命令行,书写在独立行的一条命令是一个独立的 shell 命令行。因此:在一个规则的命令中命令行 “cd”改变目录不会对其后面的命令的执行产生影响。就是说之后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。
当需要让一条命令的结果应用到下一条命令时,需要用分号隔开而不是另起一行。
#示例一:错误,第一条命令的结果不会应用到吓一跳指令
exec:
cd /home/hchen
pwd
#示例二:正确,先进入文件夹再执行pwd
exec:
cd /home/hchen;pwd
如果想把一个完整的shell命令行书写在多行上,需要使用反斜杠 (\)来对处于多行的命令进行连接,表示他们是一个完整的shell命令行
main:main.c
gcc -o \
main main.c
GNU make 支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令结束之后才能够开始执行。不过可以通过 make 命令行选项 “-j” 或者 “–jobs” 来告诉 make 在同一时刻可以允许多条命令同时执行。
当 make 读取到 “include” 关键字的时候,会暂停读取当前的 Makefile,而是去读 “include” 包含的文件,读取结束后再继读取当前的 Makefile 文件。Makefile中的include命令与C语言中的include命令类似,命令include file.dep
,即把file.dep文件在当前Makefile文件中展开
“include” 使用的具体方式如下:
include
在一个大的工程文件中,不同的文件按照功能被划分到不同的模块中,也就说很多的源文件被放置在了不同的目录下。每个模块可能都会有自己的编译顺序和规则,如果在一个 Makefile 文件中描述所有模块的编译规则,就会很乱,执行时也会不方便,所以就需要在不同的模块中分别对它们的规则进行描述,也就是每一个模块都编写一个 Makefile 文件,这样不仅方便管理,而且可以迅速发现模块中的问题。这样我们只需要控制其他模块中的 Makefile 就可以实现总体的控制,这就是 make 的嵌套执行。
subsystem:
cd subdir && $(MAKE)
#或者
subsystem:
$(MAKE) -C subdir#make -C dir:命令进入指定的目录dir中执行make
这个例子可以这样来理解,在当前目录下有一个目录文件 subdir 和一个 Makefile 文件,子目录 subdir 文件下还有一个 Makefile 文件,这个文件是用来描述这个子目录文件的编译规则。使用时只需要在最外层的目录中执行 make 命令,当命令执行到上述的规则时,程序会进入到子目录中执行 make。这就是嵌套执行 make,我们把最外层的 Makefile 称为是总控 Makefile。
$(MAKE):
make 定义了很多默认变量,像常用的命令或者是命令选项之类的。$(MAKE)就是预设的 make 这个命令的名称(或者路径)。
除此之外还有CC(C编译器的名称,默认为gcc)CPP(C预编译器的名称,默认为$(CC)-E)等
linux中一行执行多条指令: