Makefile/make
项目一开始用的autotool一套,但是这套东西太重了,出现的比较久远,依赖的东西也比较多,学习成本高,编译效率也没有新出现的ninja等工具好,所以准备换掉。
可选项有cmake和ninja/meson,不过考虑到项目还没有大到使用这些工具,目前直接更换为makefile自己手动维护。
include
编译项目时,肯定会有很多模块(比如类库)需要单独编译,或者有公共的内容(就像c的头文件),如果不想每个makefile文件都写一遍,可以使用include
比如有一个源文件a.c,每个模块都需要,那么可以定义一个变量SRC,在其他模块设置完需要编译的源码后,引入公共makefile文件,加上a.c
pulbic.mk
SRC += a.c
Makefile
SRC = m1.c m2.c
include public.mk
-include
引入的文件不存在,会报错退出。如果不想报错退出,可以在开头增加-,忽略错误。
指定路径
很多时候,编译一个模块,其头文件在一个目录(include),源文件在一个目录(src),编译时需要指定目录。如果不想指定目录,可以使用VPATH。
VPATH
VPATH = src:../headers
指定搜索src和../headers
https://www.gnu.org/software/make/manual/html_node/General-Search.html
vpath
小写的vpath可以有更多功能,支持过滤,比如指定把某个目录下的某些文件加入到搜索重。
vpath %.h ../headers
匹配../headers下的所有.h文件
vpath可以有多个
vpath %.c foo
vpath % blish
vpath %.c bar
相同的匹配规则可以写在一起
vpath %.c foo:bar
vpath % blish
注意
虽然上面可以找到源文件,但是用gcc编译时,仍然报错,因为gcc并不是makefile的命令,所以makefile只是负责执行这条语句,不知道如何增加目录。解决方法就是:要么使用makefile提供的规则,自行推到;要么手动增加路径
VPATH = src:header
test : main.o a.o
gcc -o main main.o a.o
main.o : main.c a.h
gcc -c $<
a.o : a.c a.h
gcc -c $<
.PHONY: clean
clean:
rm *.o main
上面规则自行推到完就是gcc -c src/main.c
注意
虽然上面可以找到源文件了,还是会报错,因为找不到头文件。还是那句话,gcc不是makefile的命令,不知道如何使用,我们需要自己明确指出头文件的引用位置(这个makefile没办法自动生成了)
VPATH = src:header
test : main.o a.o
gcc -o main main.o a.o
main.o : main.c a.h
gcc -c $< -I header/
a.o : a.c a.h
gcc -c $< -I header/
.PHONY: clean
clean:
rm *.o main
https://www.gnu.org/software/make/manual/html_node/Selective-Search.html
make指定目录
make -C fold
在执行make前切换到fold目录,执行完成后会切换到原目录。
环境变量传递
在makefile文件中也可以使用make指定目录继续编译;在makefile文件中,同样可以使用export设定环境变量。在同一个makefile文件中设定的环境变量,即使切换到下一个makefile继续编译,也是有效的。
设置编译参数
在编译时,有些参数可能是大家都需要的,为了方便,可以不用在每个gcc编译规则增加,而统一设置到对应的变量中。编译时会根据编译规则(是gcc还是g++)自动添加。
- CFLAGS c语言编译参数,也就是gcc后面跟的参数
- CXXFLAGS c++编译参数,也就是g++后面跟的参数
- CPPFLAGS c预处理器参数,是预处理器,上面的是编译器,一般用作头文件引入,对gcc/g++都起作用
- LDFLAGS 指定链接库的路径,也就是-L参数
- LDLIBS 指定链接库的名称,也就是-lxx参数
示例
LDFLAGS += -L/usr/lib/x86_64-linux-gnu
LDLIBS += -lncurses
注意
对于加载类库,可以写到CFLAGS/CXXFLAGS中,同样也可以都写在LDFLAGS中。不过为了更清晰,编译管理和阅读,建议搭建按照规定编写。
all
all命令表示没有指定任何特定目标时,就会使用all,相当于make的默认命令。
all: hello
hello: hello.c
gcc -o hello hello.c
clean:
rm -f hello hello.o
执行make后,会走到all,all依赖hello,走到hello,hello依赖hello.c,如果发现hello.c有修改,就执行gcc -o hello hello.c
makefile是依赖依赖规则,通过这套规则一次遍历需要生成的目标,确定其依赖的内容,判断该目标是否需要执行指定的命令。
头文件依赖
makefile会根据a.c推到出依赖的头文件a.h,但是正常项目中,都会依赖很多模块的头文件。如果我们不在makefile文件中明确指出,当头文件修改时,相关的源文件并不会重新编译。因为makefile规则很简单,你指定的依赖项,才会检测,不指定就不检测。虽然提供了很多规则匹配,但是无法满足这个需求,因为源码中依赖哪个头文件是没有规律的。
这时可以用gcc的-M参数,如果不想列出系统头文件,可以使用-MM
官方示例
DEPDIR := .deps
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.Td
POSTCOMPILE = mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d && touch $@
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
%.o : %.c
%.o : %.c $(DEPDIR)/%.d | $(DEPDIR)
$(COMPILE.c) $(OUTPUT_OPTION) $<
$(POSTCOMPILE)
$(DEPDIR): ; @mkdir -p $@
DEPFILES := $(SRCS:%.c=$(DEPDIR)/%.d)
$(DEPFILES):
include $(wildcard $(DEPFILES))
DEPDIR
定义的存放依赖文件的目录
DEPFLAGS
gcc的标签,用于生成依赖文件
-MT target
指定输出目标的名称
$ gcc -MM test.c -I ../include
test.o: test.c ../include/com.h
$ gcc -MT a -MM test.c -I ../include
a: test.c ../include/com.h
没有指定的时候,会按照输入的文件名,按照默认规则生成目标名test.o,如果指定,就按照指定的target输出。
-MMD
与-MD作用一样,只不过去掉了系统的头文件。与-MM的区别就是,-MMD会生成依赖,并且编译,-MM只会生成依赖,不编译
$ gcc -MT a -MMD test.c -I ../include
$ ls
Makefile test.c test.d
$ cat test.d
a: test.c ../include/com.h
执行后,没有任何命令输出到终端,ls可以看到当前目录下有一个test.d文件,查看内容就是代码的头文件依赖规则。
-MP
为每个依赖的头文件也提供伪目标。如果删除了文件,但是没更新依赖,也不会报错。
$ gcc -MT a -MMD -MP test.c -I ../include
$ cat test.d
a: test.c ../include/com.h
../include/com.h:
我们看到多了一行,就是把所有依赖的都文件也增加一个目标,如果文件不存在也不会报错。
-MF
指定输出的文件名
$ gcc -MT a -MMD -MP test.c -I ../include -MF a.d1
$ ls
a.d1 Makefile test.c
$ cat a.d1
a: test.c ../include/com.h
../include/com.h:
文件名由默认规则定义的test.d变成了我们指定的a.d1
POSTCOMPILE
这个是做什么用的呢?官方给出了解释,说是在生成依赖文件时退出/报错或者gcc设置的时间戳不正确,导致依赖文件是损坏的,下次使用会有问题。这里就是先把依赖文件设置为xxx.Td,如果成功了,再运行这条命令mv为xxx.d。
%.o : %.c
makefile中有默认规则,也就是不指定如何编译,makefile也可以推导出。
$ make -f Makefile2
cc -I ../include -c -o test.o test.c
$ ls
Makefile Makefile2 test.c test.o
$ cat Makefile2
all: test.o
CPPFLAGS=-I ../include
make -f
可以指定使用哪个makefile文件。
上面使用Makefile2编译,该文件中并没有指定test.o如何来的,但是可以编译成功。就是因为makefile有默认规则,看到test.o,就会找test.c,如果找到,就使用gcc编译。
如果不想使用默认规则,或者想自己定义(重载)默认规则,需要先写出默认规则,然后设定其为空。这里就是这个作用。
如果不把默认规则去除,当依赖test.o时,就会使用默认规则,而不会使用我们自定义的规则。
%.o : %.c $(DEPDIR)/%.d | $(DEPDIR)
自定义规则,增加对目录和文件的依赖。
targets : normal-prerequisites | order-only-prerequisites
|用来区分是normal prerequisites和order only prrequisites。前面的是normal prerequisites,后面的是normal prerequisites。
- normal prerequisites表示依赖条件(文件)更新了,就需要重新编译。最常用的源文件依赖。
- order only prerequisites表示文件不存在才重新编译,存在就不再编译,即使文件内容更新(比如对类库的依赖)。
$(DEPDIR): ; @mkdir -p $@
这条规则意义很明确,就是创建依赖的目录。
分号
在makefile中,每条命令都是依次执行,需要前面是tab区分(如果不是tab,而是4个空格,会报错)。如果不想换行,可以使用分号区分,这里就是没有换行,所以加了一个分号。
$(DEPFILES):
这条语句就是我们上面提到的,定义一个依赖文件的伪标签,避免依赖文件不存在而报错
include $(wildcard $(DEPFILES))
把依赖文件导入进来
依赖文件中也定义了如何生成指定的目标文件,我们makefile中也定义了如何生成目标文件。当出现相同目标规则时,makefile会合并,这样就起到把依赖包加载进来的作用了。
不过官方给的这个例子只是告诉我们如何生成依赖文件,并不是完整的makefile,直接使用会有问题
- 这个示例无法直接运行,提示找不到目标,因为必须要指定一个目标,
%o : %c
并不能作为目标。 - 给的示例代码中没有SRCS,DEPFILES是空。编译提示没有.deps/test.d,报错退出。
https://gcc.gnu.org/onlinedocs/gcc-10.5.0/gcc/Preprocessor-Options.html
https://make.mad-scientist.net/papers/advanced-auto-dependency-generation/
完整示例
.PHONY: all clean
SRCS=$(wildcard *.c)
OBJS=$(SRCS:.c=.o)
DEPDIR := .deps
CPPFLAGS += -I ../include
CFLAGS += -std=c99
DEPFLAGS = -MT $@ -MMD -MP -MF $(DEPDIR)/$*.Td
POSTCOMPILE = mv -f $(DEPDIR)/$*.Td $(DEPDIR)/$*.d && touch $@
COMPILE.c = $(CC) $(DEPFLAGS) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
all: bin
bin: $(OBJS)
gcc $^ -o $@
%.o : %.c
%.o : %.c $(DEPDIR)/%.d | $(DEPDIR)
$(COMPILE.c) $(OUTPUT_OPTION) $<
$(POSTCOMPILE)
$(DEPDIR): ; @mkdir -p $@
DEPFILES := $(SRCS:%.c=$(DEPDIR)/%.d)
$(DEPFILES):
include $(wildcard $(DEPFILES))
clean:
-rm *.o
-rm bin
-rm -rf .deps
针对官方的例子,做了完善。
- 增加SRCS,获取当前目录下所有.c文件。wildcard是makefile的函数,用正则表达式获取指定规则的文件名。$(wildcard *.c)就是获取所有.c结尾的文件
- 增加OBJS,得到所有c文件对应的中间文件.o的名称。$(SRCS:.c=.o)就是一个字符串替换规则,把SRCS中所有的.c替换成.o
- 增加all目标,依赖bin
- 增加bin目标,依赖所有的.o。
gcc $^ -o $@
中$^
是makefile的通配符规则,表示所有的依赖项,也就是bin目标冒号后面所有的依赖项;$@
表示所有的目标,也就是bin,如果有多个目标,就是多个。
流程解析
我们最终目的是为了解决,动态的导出依赖项,避免有的头文件修改了没有重新编译。
整体思路就是通过gcc获取所有依赖,然后导入到makefile中。
上面的makefile文件如何实现的呢?
首次编译
由于是第一次编译,所有我们的源码肯定会编译,也不用考虑依赖头文件的问题。
- 先解析到all目标,然后到bin目标,bin依赖所有的.o,找到创建.o的规则。
- .o依赖.c \((DEPDIR)/%.d和\)(DEPDIR)。.c \((DEPDIR)/%.d文件发生修改就会重新编译。\)(DEPDIR)如果不存在才会重新编译
- $(DEPDIR)规则创建了依赖的目录
- $(DEPFILES):创建了空规则,避免第一次没有生成.d文件而报错
- include $(wildcard $(DEPFILES))就是引入.d文件。include也是比较特殊的功能。并不是只会运行到这里的时候引入依次。它可以认为是一直保证引入最新的文件。比如第一次运行到这里,还没有创建.d文件,理论上应该报错,实际上却没有,因为在最后目标完成时创建了.d文件。并且如果中间更新了.d文件,也会把最新的加载进来。
- clean中的rm增加-,是因为避免rm删除时,目录下没有对应文件而报错
- .PHONY: all clean。.PHONY是伪目标。做什么用呢?就是比如你当前目录创建了一个clean文件。会导致make clean永远不执行。因为发现clean文件有了,所以认为makeflie中的clean目标满足了,并且clean目标又没有依赖,就认为一直满足条件,就不会执行。而增加了伪目标,makefile就不会检测文件是否存在并且是否是最新,而会每次都执行。
如果删除了.deps
如果删除了保存依赖文件的目录.deps,就会触发规则%.o : %.c $(DEPDIR)/%.d | $(DEPDIR),就会创建目录,编译新的.o,由于.o重新编译,就会编译新的bin
如果删除了.d
如果删除了.deps下保存依赖关系的.d文件,就会触发规则%.o : %.c $(DEPDIR)/%.d | $(DEPDIR),创建新的.o和bin
如果修改了头文件
如果修改了头文件,因为include了保存头文件依赖关系的.d文件,就可以检测到,重新编译.o
如果增加了头文件
如果在源码中增加了新的依赖,虽然.d文件中还没有当前的依赖,但是源代码.c文件发生了变化,也会重新编译。
多目标规则
%.o: %.c
必须有.
,如果没有.
,匹配规则不起作用。
https://www.gnu.org/software/make/manual/html_node/index.html#SEC_Contents