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

posted @ 2023-06-01 09:17  秋来叶黄  阅读(106)  评论(0编辑  收藏  举报