概述
关于Makefile 的功能大家或多或少都知道一些,本不想多写这一段,但是总觉得不写点啥,会有一些怪怪的,所以还请大家见谅。
先说下Makefile 的好处:
“自动化编译”,简单来说就是把原先需要一步一步手动编写的gcc 编译命令,都集合在一个文件内,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。
Makefile可以做哪些事情: Makefile 文件描述了整个工程的编译、连接等规则。
- 工程中的哪些源文件需要编译以及如何编译;
- 需要创建那些库文件以及如何创建这些库文件;
- 如何最后产生我们想要的可执行文件;
编译+连接
在做GCC 编译的时候,会先.c 文件编译成.o 文件,之后在通过ld 文件将.o 文件连接成最后的可执行文件。在编译时,编译器会检测程序语法,函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会提示“Linker Error”。
makefile基本格式
makefile基本格式如下:
target ... : prerequisites ...
command
...
...
其中:
target - 目标文件, 可以是 Object File, 也可以是可执行文件
prerequisites - 生成 target 所需要的文件或者目标
command - make需要执行的命令 (任意的shell命令),如果其不与“target:prerequisites”在一行,那么,必须以[Tab]开头,如果和prerequisites在一行,那么可以用分号做为分隔
make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
make工作流程
在默认的方式下,也就是我们只输入make命令。那么,
- make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target),并把这个文件作为最终的目标文件。
- 如果目标文件不存在,或是目标文件所依赖的后面的 .o文件的文件修改时间要比目标文件这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
- 如果目标文件所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即,如果在我找了依赖关系之后,冒号后面的文件还是不在,那么对不起,我就不工作啦。
简单举例
我们用一个例子来做个说明。在这个例子中,我们有一个主程序代码(main.c)、两份函数代码(sample1.c、sample2.c)以及几个头文件。通常情况下,我们需要这样编译它:
gcc -o hello main.c sample1.c sample2.c
如果没有makefile,在开发+调试程序的过程中,我们就需要不断地重复输入上面这条编译命令,要不就是通过终端的历史功能不停地按上下键来寻找最近执行过的命令。这样做两个缺陷:
一旦终端历史记录被丢失,我们就不得不从头开始;
任何时候只要我们修改了其中一个文件,上述编译命令就会重新编译所有的文件,当文件足够多时这样的编译会非常耗时。
那么Makefile又能做什么呢?我们先来看一个最简单的makefile文件:
hello: main.c sample1.c sample2.c
gcc -o hello main.c sample1.c sample2.c
现在你看到的就是一个最基本的Makefile语句,它主要分成了三个部分,第一行冒号之前的hello,我们称之为目标(target),被认为是这条语句所要处理的对象,具体到这里就是我们所要编译的这个程序hello。冒号后面的部分(main.c sample1.c sample2.c,我们称之为依赖关系表,也就是编译hello所需要的文件,这些文件只要有一个发生了变化,就会触发该语句的第三部分,我们称其为命令部分,相信你也看得出这就是一条编译命令。现在我们只要将上面这两行语句写入一个名为Makefile或者makefile的文件,然后在终端中输入make命令,就会看到它按照我们的设定去编译程序了。
接下来,让我们来解决一下效率方面的问题,先初步修改一下上面的代码:
cc = gcc
prom = hello
source = main.c sample1.c sample2.c
$(prom): $(source)
$(cc) -o $(prom) $(source)
如你所见,我们在上述代码中定义了三个常量cc、prom以及source(请注意:因为它们在整个文件的执行过程中并不是可更改的,作用也仅仅是字符串替换而已,非常类似于C语言中的宏定义)。它们分别告诉了make我们要使用的编译器、要编译的目标以及源文件。这样一来,今后我们要修改这三者中的任何一项,只需要修改常量的定义即可,而不用再去管后面的代码部分了。
但我们现在依然还是没能解决当我们只修改一个文件时就要全部重新编译的问题。而且如果我们修改的是hello.h文件,make就无法察觉到变化了(所以有必要为头文件专门设置一个常量,并将其加入到依赖关系表中)。下面,我们来想一想如何解决这个问题。考虑到在标准的编译过程中,源文件往往是先被编译成目标文件,然后再由目标文件连接成可执行文件的。我们可以利用这一点来调整一下这些文件之间的依赖关系:
cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
main.o: main.c $(deps)
$(cc) -c main.c
sample1.o: sample1.c $(deps)
$(cc) -c sample1.c
sample2.o: sample2.c $(deps)
$(cc) -c sample2.c
这样一来,上面的问题显然是解决了,但同时我们又让代码变得非常啰嗦,啰嗦往往伴随着低效率,是不祥之兆。经过再度观察,我们发现所有.c都会被编译成相同名称的.o文件。我们可以根据该特点再对其做进一步的简化:
cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
%.o: %.c $(deps)
$(cc) -c $< -o $@
在这里,我们用到了几个特殊的宏。首先是%.o:%.c,这是一个模式规则,表示所有的.o目标都依赖于与它同名的.c文件(当然还有deps中列出的头文件)。再来就是命令部分的 < 和 <和 <和@,其中 < 代 表 的 是 依 赖 关 系 表 中 的 第 一 项 ( 如 果 我 们 想 引 用 的 是 整 个 关 系 表 , 那 么 就 应 该 使 用 <代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用 <代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用^),具体到我们这里就是%.c。而$@代表的是当前语句的目标,即%.o。这样一来,make命令就会自动将所有的.c源文件编译成同名的.o文件。不用我们一项一项去指定了。整个代码自然简洁了许多。
Makefile 中很多时候通过自动变量来简化书写, 各个自动变量的含义如下:
自动变量 | 含义 |
---|---|
$@ | 目标集合 |
$% | 当目标是函数库文件时, 表示其中的目标文件名 |
$< | 第一个依赖目标. 如果依赖目标是多个, 逐个表示依赖目标 |
$? | 比目标新的依赖目标的集合 |
$^ | 所有依赖目标的集合, 会去除重复的依赖目标 |
$+ | 所有依赖目标的集合, 不会去除重复的依赖目标 |
$* | 这个是GNU make特有的, 其它的make不一定支持 |
到目前为止,我们已经有了一个不错的makefile,至少用来维护这个小型工程是没有什么问题了。当然,如果要进一步增加上面这个项目的可扩展性,我们就会需要用到一些Makefile中的伪目标和函数规则了。例如,如果我们想增加自动清理编译结果的功能就可以为其定义一个带伪目标的规则;
cc = gcc
prom = hello
deps = hello.h
obj = main.o sample1.o sample2.o
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
%.o: %.c $(deps)
$(cc) -c $< -o $@
.PHONY : clean
clean:
rm -rf $(obj) $(prom)
有了上面最后两行代码,当我们在终端中执行make clean命令时,它就会去删除该工程生成的所有编译文件。
当然如果想默认在执行build之后,先做clean,也可以将clean做如下修改:
$(prom): clean $(obj)
$(cc) -o $(prom) $(obj)
另外,如果我们需要往工程中添加一个.c或.h,可能同时就要再手动为obj常量再添加第一个.o文件,如果这列表很长,代码会非常难看,为此,我们需要用到Makefile中的函数,这里我们演示两个:
cc = gcc
prom = hello
deps = $(shell find .inc -name "*.h")
src = $(shell find .src -name "*.c")
obj = $(src: %.c=%.o)
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
%.o: %.c $(deps)
$(cc) -I$(inc_dir) -c $< -o $@
.PHONY : clean
clean:
rm -rf $(obj) $(prom)
其中,shell函数主要用于执行shell命令,obj 赋值那端语句的,其实就是一个字符替换函数,它会将src所有的.c字串替换成.o,实际上就等于列出了所有.c文件要编译的结果。有了这两个设定,无论我们今后在该工程加入多少.c和.h文件,Makefile都能自动将其纳入到工程中来。
指定输出目录
上述的例子中.o 文件都默认会在src 目录下,这样比较讨厌。我们就会想是否可以统一放在一个目录下,比如obj 这样的目录,只用来存放.o 文件. 可执行文件,放在另外一个固定的目录。请参考如下的例子:
cc = gcc
prom = hello
DIR= ($shell pwd)
inc_dir = $(DIR)/inc
src_dir = $(DIR)/src
obj_dri = $(DIR)/obj
obj = $(pathsubst $(src_dir)/%.c, $(src_dir)/%.o, $(wildcard $(src_dir)/%.c) )
output = $(DIR)/out
all: clean $(output) $(obj_dir) $(prom)
$(output):
mkdir $@
$(obj_dir):
mkdir $@
$(prom): $(obj)
$(cc) -o $(prom) $(obj)
$(obj_dir)/%.o: $(src_dir)/%.c
$(cc) -I$(inc_dir) -c $< -o $@
.PHONY : clean
clean:
rm -rf $(obj)
rm -rf %(output)/*
上述的示例,可以将所有的.o 文件放在obj目录下面,可执行文件都存放在out 目录下。