前言
在软件开发过程中,经常会用到各种IDE,也就是集成开发环境。我们只需要把源代码、头文件添加进来,再点击软件的“build”按钮,就可以完成编译。整个过程都是IDE帮我们完成的。我们本身对编译过程并不了解。在以后的工作种会逐步发现,IDE的使用会越来越少。项目都是基于脚本编译的,大部分开源项目都是基于cmake或者makefile。cmake这里就不作介绍了。他本质也是生成makefile。
基于这种情况,我们需要了解makefile的机制,也许实际应用中你不会用到,但是多了解一些总是有好处的。本文的目的就是快速写出自己的makefile,makefile其他相关特性请自行了解。话不多说,直接开始!
开始
我们先新建一个main.c。
只有一个文件,那我们直接编译。会生成a.out。
gcc main.c -o main
那我们如何使用makefile编译呢。
Makefile规则
上面我们编译时指定了-o main。也就是我们想生成可执行程序main。可执行程序就是我们的目标。而我们的目标又是依赖main.c。这里就引入了目标和依赖的概念。
目标的生成会有依赖,这个依赖也可能是一个目标。一直这么层层找下去,生成对应的依赖目标。所有的依赖都有了之后,就可以执行目标锁对应的command。
target ... : prerequisites ...
command
看到这里可能比较难理解,那我们先写一个简单的makefile看看。
main:main.o
gcc main.o -o main
main.o:main.c
gcc -c main.c -o main.o
main是我们的目标。main可执行程序依赖main.o,然后main.o又依赖main.c。然后我们执行make,就可以编译出main可执行程序。这样一个简单的makefile就写好了。
我们实际的项目不可能只有一个源文件。我们现在新增a.c、b.c。
我们现在来实现第二版makefile。
main:main.o a.o b.o
gcc main.o a.o b.o -o main
main.o:main.c
gcc -c main.c -o main.o
a.o:a.c
gcc -c a.c -o a.o
b.o:b.c
gcc -c b.c -o b.o
执行make,可以看到编译过程。
现在才3个源文件,如果100个呢,这样的makefile会非常庞大、不灵活。
我们继续优化。第三版makefile。
obj = main.o a.o b.o
main:$(obj)
gcc $^ -o $@
main.o:main.c
gcc -c main.c -o main.o
a.o:a.c
gcc -c a.c -o a.o
b.o:b.c
gcc -c b.c -o b.o
obj是一个变量,变量的内容就是main.o a.o b.o。$(obj)就是使用这个变量。所以等效如下:
main:$(obj)等效 main:main.o a.o b.o,这样要新增源文件,只需要修改obj变量,和添加一个源文件规则即可。
$^表示所有的依赖目标,也就是main.o a.o b.o。$@表示目标main。这是makefile的自动变量,先记住就好。
所以当前的makefile还不够执行,因为没增加一个文件还需要增加new.o:new.c的规则。
第四版makefile。
obj = main.o a.o b.o
main:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $^ -o $@
我们使用%通配符。当寻找main.o的时候,就会先找到main.c ,然后通过命令生成main.o。简单说就是先找main.o,然后发现%.o:%.c规则符合。所以就会展开成main.o:main.c,利用这个规则,就不需要每个文件都写一个规则了。即使要新增一个文件c.c。那么只需要修改obj = main.o a.o b.o c.o就可以了。
如果每增加一个文件都要修改obj变量,也很麻烦。
第五版makefile。
src = $(wildcard *.c)
obj = $(patsubst %.c,%.o,$(src))
main:$(obj)
gcc $^ -o $@
%.o:%.c
gcc -c $^ -o $@
$(wildcard *.c)就是查找当前目录下的所有.c文件。使用了*通配符。patsubst就是将所有的.c文件替换成.o,然后返回给obj变量。这两个函数都是makefile内置的函数。这样我们随便增加源文件,都不需要修改makefile了。
不过我们生成的中间文件(.o)都和源文件混在一起了。我们希望将所有的.o文件,存放到某个目录。
tmp = objs
src = $(wildcard *.c)
obj = $(addprefix $(tmp)/,$(patsubst %.c,%.o,$(src)))
main:$(obj)
gcc $^ -o $@
$(tmp)/%.o:%.c
gcc -c $^ -o $@
addprefix就是添加一个前缀。应为我们想将.o文件都存放到objs里,main需要依赖objs的.o文件。所以需要写成main:$(obj),展开就是main: objs/main.o objs/a.o objs/b.o。
objs/main.o和匹配规则$(tmp)/%.o匹配上。%就相当于main。然后%.c,就是main.c。目标的%内容会替换到依赖的%d。mian.c就在当前文件下,所以可以找到。
执行make,生成的.o文件都存放到objs里了(objs需要提前手动创建)。
我们现在将所有的源文件放到src目录下。
第六版makefile。
tmp = objs
d_src = src
src = $(wildcard $(d_src)/*.c)
obj = $(addprefix $(tmp)/,$(patsubst %.c,%.o,$(notdir $(src))))
$(info $(obj))
main:$(obj)
gcc $^ -o $@
$(tmp)/%.o:$(d_src)/%.c
gcc -c $^ -o $@
notdir去掉路径信息,所以obj的值还是objs/main.o objs/a.o objs/b.o。注意匹配规则改成了$(tmp)/%.o:$(d_src)/%.c。相比之前加了d_src。因为源文件放到了src目录下,如果不加$(d_src)/%.c那么编译时就会找不到对应的.c文件。
make: *** No rule to make target 'objs/a.o', needed by 'main'. Stop.
到这里就是比较比较完成的makefile了。
第六版makefile。
之前在做项目的时候,源代码有很多,并不是放在同一个目录下的。例如在src下又添加了一个test文件夹,文件夹下有t1.c和t2.c。
makefile如下。这个makefile编译的时候是有问题的。
tmp = objs
d_src = src
src = $(wildcard $(d_src)/*.c)
src += $(wildcard $(d_src)/test/*.c)
obj = $(addprefix $(tmp)/,$(patsubst %.c,%.o,$(notdir $(src))))
$(info $(obj))
main:$(obj)
gcc $^ -o $@
$(tmp)/%.o:$(d_src)/%.c
gcc -c $^ -o $@
objs/a.o objs/b.o objs/main.o objs/t1.o objs/t2.o
gcc -c src/a.c -o objs/a.o
gcc -c src/b.c -o objs/b.o
gcc -c src/main.c -o objs/main.o
make: *** No rule to make target 'objs/t1.o', needed by 'main'. Stop.
src目录下的源文件都可以编译,但是找不到objs/t1.o的依赖规则。因为我们的依赖规则写的是$(tnp)/%.o:$(d_src)/%.c。所以只会在src目录下查找t1.c,因为t1.c是在src/test目录下,所以找不到然后报错。我们可以增加一条匹配规则就可以编过了。到那时这样也比较麻烦。
$(tmp)/%.o:$(d_src)/test/%.c
gcc -c $^ -o $@
make提供了vpath的功能,可以让make时在当前目录找不到的情况下去vpath指定的路径去找。
终极版makefile。
tmp = objs
d_src = src
src = $(wildcard $(d_src)/*.c)
src += $(wildcard $(d_src)/test/*.c)
vpath %.c src src/test
obj = $(addprefix $(tmp)/,$(patsubst %.c,%.o,$(notdir $(src))))
$(info $(obj))
main:$(obj)
gcc $^ -o $@
$(tmp)/%.o:%.c
gcc -c $^ -o $@
vpath指定了src src/test目录,所以make时会从这些目录去查找。那么匹配规则就可以改成$(tmp)/%.o:%.c。
这样的好处就是你的源文件目录结构可以是多层嵌套的,并且可以将生成的中间文件存放到objs目录。网上搜索make将生成的.o文件存放到指定目录,99.9%的文章都是以源代码在src目录下举例说明的,对于多层目录结构并不使用。我也是研究了很久才实现的。可以说是全网描述最清楚的文档。
自此,终极版makefile已经满足入门的大部分需求了。具体的特性可以另行学习。