文章目录
引入
拿之前文件拷贝的例子来说,它们的结构是这样的:
现在要使用源文件io.c
和cp.c
编译生成一个可执行文件,然后实现文件之间的拷贝,假设这是一个工程代码,cp.c
和io.c
是由两个人来完成的,所以现在要将源文件编译生成目标文件来检查语法是否有错,然后将两个目标文件进行链接生成可执行文件。
gcc -c src/io.c -o obj/io.o -I include/
gcc -c src/cp.c -o obj/cp.c -I include/
gcc -o bin/cp obj/io.o obj/cp.o
可见只有两个文件也需要敲三条命令,如果现在工程里有成百上千的源文件和目标文件,那么岂不是还要敲这么多指令?而且有些文件是依赖于别的文件,所以它们的编译顺序也有讲究,有些文件可能是目标文件它就不需要编译了,有些文件是源文件需要编译,所以它们的编译规则也很重要。可见如果工程十分庞大的话,使用敲指令的方法十分麻烦。所以现在引入make
和Makefile
,将指令写入到Makefile
文件中,然后直接执行make
就可以完成对项目工程的编译。
Makefile文件
将终端执行的指令放到这个Makefile
文件中去, 这个文件中就描述了编译规则和依赖关系等信息。通过Makefile
就可以对整个软件工程进行自动编译,极大地提高了软件开发的效率。
make工具
当将这个Makefile
文件编写完毕后就要执行这个文件,因为这个文件相当于对终端上输入的指令进行批量操作,不需要手敲指令,只要执行这个文件就等同于在终端上敲入指令。那么要执行这个文件就需要make
工具。这个Makefile
文件其实有点类似于shell
脚本,在shell
脚本里放置在终端的指令,然后给这个脚本赋予一个可执行的权限,执行这个脚本就相当于在终端上敲这些指令。
Makefile的编写原则和规则
- 当
make
命令不带选项运行时,它从Makefile
中读取制定规则 - 当制定规则在不同于
Makefile(或makefile)
的其他文件中时,就要运行带有-f
选项的make
命令,比如文件名为test.Makefile
,在读取规则时就要执行make -f test.Makefile
Makefile的编写规则一
目标列表:关联性列表
<TAB>命令列表
- 目标列表:可以理解为一个最后编译链接后的一个可执行文件或者是一个编译出来的一个目标文件(目标列表是用一个或多个空格分开的目标文件的清单,意思就是可以生成一个可执行文件,也可以生成多个可执行文件)
- 关联性列表(也称先决条件):同样是用一个或多个空格分开的目标文件,是目标文件所依赖的多个目标文件的清单。例如有一个源文件
test.c
,现在要将它编译成一个目标文件test.o
,那么它的依赖文件就是test.c
,或者使用指令gcc -o test test.c
后生成一个可执行文件,那么这个文件就是依赖源文件test.c
生成的。 - 命令列表:用于创建目标文件的将要执行的命令清单,这些命令被换行符分开。命令列表中的每个命令必需以字符开始。就拿上边的例子来说,要从源文件
test.c
变成一个可执行文件test
就需要通过一些命令例如gcc
或者shell
指令等,这里的命令就是命令列表。
Makefile的编写规则二(一般使用第一种编写规则)
目标列表:关联性列表;命令列表 |
---|
- 命令列表是一系列被分号隔开的命令。一个很长的命令可以分隔符隔开
- 例如:
cd /home/dx/my_project/system_programming;rm file1 rm file2
这里的命令可以使用反斜杠\
隔开 - Makefile中可使用
shell
中的一些通配符- 注释(#)、连接符(\)、通配符(? *)
make的使用语法
常用选项 | 特性 |
---|---|
-d | 显示调试信息 |
-f <文件> | 指定从哪个文件中读取依赖关系信息。默认是从Makefile或者makefile中读取 “-”表示从标准输入 |
-h | 显示所有选项的简要说明 |
-n | 仅输出要执行的命令,而不实际执行它们 |
安静的方式运行,不显示任何信息 |
示例–Makefile和make的使用
-
首先在目录下新建四个目录
源文件存放在
src
目录下,头文件存放到include
下,可执行文件存档到bin
目录下,目标文件存放到obj
目录下 -
然后编写源代码(一个简单的加减乘除程序)
-
然后编写一个Makefile文件来管理工程代码
#书写规则:将最后生成的可执行文件放在最上边,然后指定它所依赖的文件 bin/calc:obj/math.o obj/mycalc.o gcc -o bin/calc obj/math.o obj/mycalc.o obj/math.o:src/math.c gcc -o obj/math.o -c src/math.c obj/mycalc.o:src/mycalc.c gcc -o obj/mycalc.o -c src/mycalc.c -I include/ #这里的mycalc.c文件里用到include目录下的头文件,所以要指定头文件的目录
在编写
Makefile
文件的时候一定要注意文件名和目录名,如果编写错误,最后的make
会报错。Makefile
文件编写完成以后,通过make
工具来执行Makefile
文件中的规则和依赖关系。通过查看可以发现通过
make
工具生成了目标文件和可执行文件,并且最后生成的可执行文件也能够执行。
依赖树
make的一些注意事项
-
首先在编写
Makefile
文件的时候,每一条命令列表前边都要加一个tab键,如果不加tab键会报错; -
在
Makefile
中一般将可执行文件放到最上边,然后整体呈现一种倒推的方式进行编写。通过观察make
的执行顺序和Makefile
的编写顺序,发现虽然说可执行文件写在第一行但是它会去找到它依赖的文件,然后要先生成依赖文件后再回来链接成可执行文件。
这里可执行文件先写的依赖是obj/math.o
文件,所以在make
的时候就会先生成这个目标文件,如果将它的依赖调换一下就会先生成obj/mycalc.o
的目标文件。
-
当对同一个工程代码进行
make
两次的时候就提示/bin/calc
已是最新,不会再进行编译。这是因为make
工具会根据编写的规则去解析这些文件,根据这些文件的时间戳去判断是否需要对源文件进行编译链接等操作。这里为了验证这一现象,可以对源文件敲一个回车再删掉,给make
工具一个错觉让它以为源文件发生了修改从而重新编译链接生成可执行文件。如图所示,正常的话如果里边有生成对应的文件,然后再次进行
make
肯定会显示xxx已是最新
,但是由于现在对源文件进行编辑,所以它的时间戳更新为当前的时间,而make
去解析源文件后,发现它的时间戳更新了,所以认为它是一个已被更改的文件,所以它会重新编辑链接生成可执行文件。通过make
这个工具,可以对文件进行选择性的编译,只编译更改的文件,大大地简化了编译过程,提高了编译效率。就像keil
工具里边的build
按钮一样只编译改动过的文件。
Makefile的变量使用
简单变量
-
定义
这里的文本内容实际上就是字符串
-
添加(如果已经定义了一个变量还想要追加内容的话可以使用添加的方式)
上述两种方法等价,第二种方法在进行编写的时候中间要加空格隔开
-
引用变量
- $(变量名),例如
CC:=gcc $(CC) -o test test.c
- $单字符变量,例如
C:=gcc $C -o test test.c
- $(变量名),例如
示例–对上边的Makefile进行修改,利用变量定义的方式简化内容
为了防止将原来的Makefile
文件进行覆盖,所以将此文件拷贝为Makefile2
。由于文件名不是标准的Makefile
或者makefile
,所以这里在运行Makefile2
的时候要加-f
参数来指定文件
CC:=gcc
TARGET:=bin/calc
DEPEND:=obj/mycalc.o
DEPEND+=obj/math.o
CFLAGS:=-Iinclude
CFLAGS+=-c
#书写规则:将最后生成的可执行文件放在最上边,然后指定它所依赖的文件
$(TARGET):$(DEPEND)
$(CC) -o bin/calc obj/math.o obj/mycalc.o
obj/math.o:src/math.c
$(CC) -o obj/math.o $(CFLAGS) src/math.c
obj/mycalc.o:src/mycalc.c
$(CC) -o obj/mycalc.o $(CFLAGS) src/mycalc.c
#这里的mycalc.c文件里用到include目录下的头文件,所以要指定头文件的目录
这里需要注意几点:
- 在
Makefile
中使用变量替换原字符串以后,后边使用变量名要加$
符号,要不然就会报错; - 依据上边的代码,在进行追加写入的时候一定要使用
+=
而不是:=
,如果使用:=
就会将之前的覆盖掉,所以它真正有效的应该是写在后边的变量定义; - 在进行定义变量的时候一般不需要在
:=
两边敲空格,因为空格也会被当作字符作为输入; - 变量名一般使用大写来定义
最后的执行结果如下:
内置变量
通过上边的变量定义简化了一些操作,但是后边编译用到的文件看着还是比较繁琐
那么在Makefile
中有几个内部变量来帮助简化这些操作,常用的内边变量和定义如下:
变量名 | 意义 |
---|---|
$@ | 当前目标的名称 |
$? | 比当前目标更新的已修改的依赖性列表 |
$< | 依赖性列表中的第一个文件 |
$^ | 用空格分开的所有依赖性列表 |
示例–使用Makefile的内置变量对上边的Makefile进行修改
CC:=gcc
TARGET:=bin/calc
DEPEND:=obj/mycalc.o
DEPEND+=obj/math.o
CFLAGS:=-Iinclude
CFLAGS+=-c
#书写规则:将最后生成的可执行文件放在最上边,然后指定它所依赖的文件
$(TARGET):$(DEPEND)
$(CC) -o $@ $^
obj/math.o:src/math.c
$(CC) -o $@ $(CFLAGS) $<
obj/mycalc.o:src/mycalc.c
$(CC) -o $@ $(CFLAGS) $<
#这里的mycalc.c文件里用到include目录下的头文件,所以要指定头文件的目录
虚目标
-
不存在的文件,而且也无需创建它们;
-
允许强制执行某些事件,而这些事件在正常的规则中是不会发生的;
-
虚目标不是真正的文件,make命令可以使用针对它们的任意规则
-
常见的虚目标列表
目标 意义 all 生成工程中所有可以执行者,通常是Makefile的第一个生成目标 test 运行程序的自动测试套件 clean 删除make all生成的所有文件 install 在系统目录中安装工程项目生成的可执行文件和文档 uninstall 删除make install安装的所有文件
我的理解就是虚目标是make
工具用来执行一些指令的,比如之前在生成目标文件和可执行文件后,再次make
就会提示已是最新并不会编译,这时候就需要把文件删除后再执行make
,通过虚目标就可以使用make
指令执行相关的操作来调用shell
指令来将相关的文件删除。
应用场景1:删除目标文件和可执行文件
clean:
rm -rf $(TARGET) $(DEPEND)
#再上边文件最后添加这一句话
运行的时候指令为make clean -f Makefile2
就可以清除目标文件和可执行文件
需要注意的是如果在当前目录下如果有这样的一个同名的文件,那么执行make clean -f Makefile2
就会提示XXX已是最新
,就不会去执行Makefile
里边的虚目标了。这个解决方法可以通过后边的特殊目标来解决这个问题。
应用场景2:调试信息
在编写Makefile
的过程中其实还是比较容易出错的,因为它不像在命令行那样子可以使用tab键补齐,需要手动的一个一个敲。一个不小心可能就会敲错,然后导致最终编译不过的问题,所以这里可以使用虚目标来进行调试信息的打印,将变量名引用的字符串打印出来以方便进行排错。
CC:=gcc
TARGET:=bin/calc
DEPEND:=obj/mycalc.o
DEPEND+=obj/math.o
CFLAGS:=-Iinclude
CFLAGS+=-c
#书写规则:将最后生成的可执行文件放在最上边,然后指定它所依赖的文件
$(TARGET):$(DEPEND)
$(CC) -o $@ $^
obj/math.o:src/math.c
$(CC) -o $@ $(CFLAGS) $<
obj/mycalc.o:src/mycalc.c
$(CC) -o $@ $(CFLAGS) $<
#这里的mycalc.c文件里用到include目录下的头文件,所以要指定头文件的目录
debug:
echo $(CFLAGS)
echo $(TARGET)
echo $(DEPEND)
.PHONY:clean
clean:
rm -rf $(TARGET) $(DEPEND)
它的用法和上边的clean
类似,在执行的时候指定debug
选项,然后将调试信息打印出来方便排错。如果不想要打印命令只打印字符串可以在前边加一个@,如@echo $(CFLAGS)
就可以避免将指令打印出来。
特殊目标
make
中有一些预定义的目标,这些预定义目标被make
以一种特殊的方式进行处理,这些目标被称为特殊目标
这里将使用.PHONY
将clean
声明成一个虚目标后,就不会去关联clean
这个文件了,也能够正常的将目标文件和可执行文件删除。
默认模式规则
在上面的Makefile
文件中有两行指令是将.c
源文件编译成.o
目标文件,由于这里只有两个源文件所以对应两条指令,如果这里的源文件数量比较多,那么写那么多条指令显然不现实。那么此时就要用到make
的默认模式规则。
常用的默认的模式规则(GNU make)
%.o:%.c:
$(CC) $(CFLAGS) -c $<
#这里的%号是通配符的意思,表示所有的.o目标文件都依赖于所有的.c源文件,make工具会自动根据依赖关系进行查找,例如test.o:test.c calc.o:calc.c
#$(CC)是编译工具,$(CFLAGS)是编译指定的选项,-c参数是只进行编译不进行链接,$<表示依赖文件列表里边的第一个依赖项
示例–将上边由源文件编译生成目标文件的方法改为使用模式规则的方法
CC:=gcc
TARGET:=bin/calc
DEPEND:=obj/mycalc.o
DEPEND+=obj/math.o
CFLAGS:=-Iinclude
CFLAGS+=-c
#书写规则:将最后生成的可执行文件放在最上边,然后指定它所依赖的文件
$(TARGET):$(DEPEND)
$(CC) -o $@ $^
obj/%.o:src/%.c
$(CC) -o $@ $(CFLAGS) $<
#obj/目录下的所有.o目标文件依赖于src/下的所有源文件,make工具根据依赖规则生成对应的目标文件
#obj/math.o:src/math.c
# $(CC) -o $@ $(CFLAGS) $<
#obj/mycalc.o:src/mycalc.c
# $(CC) -o $@ $(CFLAGS) $<
#这里的mycalc.c文件里用到include目录下的头文件,所以要指定头文件的目录
debug:
echo $(CFLAGS)
echo $(TARGET)
echo $(DEPEND)
.PHONY:clean
clean:
rm -rf $(TARGET) $(DEPEND)