在触摸Linux之前,我从前做过一段时间的裸机嵌入式开发,那个时分我并不知道什么是Makefile,因为Windows的IDE帮我做好了构建整个工程的作业,我要做的就像是往搭好框架的房子里码砖。当触摸Linux后,我就不得不自己编写Makefile来构建工程了,会不会编写Makefile也就从一个旁边面说明一个工程师是否有构建大型工程的才能。这儿记载一下本人的学习Makefile的进程,本文主要参考陈皓大神的《跟我一同写Makefile》,和GNU Make文档。

0 前言

Makefile所做的作业便是“主动化编译”,一旦写好只需一个make指令,整个工程即彻底主动编译。本文默许编译器为Unix下gcc,以C言语的源码作为基础。在这儿,我并不想长篇叙述Makefile的写法,假如想了解更详细的常识的,还请移步陈皓大神的博客。

首先咱们需求知道程序的编译和链接的相关常识,这个能够参考我的博客《C言语编译原理浅析》和《链接》。咱们知道,源文件经过编译工具链(如gcc)编译、链接能够生成可履行文件。而巨大的软件工程可能具有数量巨大的源文件,这使得在指令行中输入指令生成可履行文件的办法变得不那么容易接受。而make便是用来完成这项作业的工具,make指令更像是Makefile文件的解释器,会依据Makefile文件的编写逻辑生成可履行文件。

最初我学习Makefile编写的时分,参照的是陈皓大神的《跟我一同写Makefile》,他从基础处讲起,一点点深化,逐步介绍Makefile的各种特性和编写规矩,使我一个小白收益匪浅,也写出了人生第一份Makefile。今天我预备以一种倒叙的逻辑写这篇博客,首先我会给一个能够运用的Makefile比如,然后一步步解析这个比如中所用的Makefile规矩。

1 一个比如

在下面这个比如中,生成一个名为test的可履行文件,根本涉及Makefile的根本规矩、隐晦规矩、变量、函数运用等,将在接下来的内容中一一介绍。

# define the target
TARGET = test
# define the Build Directory
BUILD_DIR = build
OBJ_DIR := $(BUILD_DIR)/objs
DEP_DIR := $(BUILD_DIR)/deps
# define PATH
LOCAL_PATH = $(shell pwd)
# define the sources and objects
SOURCES := $(shell find $(LOCAL_PATH)/ -name "*.c")
OBJS := $(addprefix $(OBJ_DIR)/, $(patsubst %.c, %.o, $(notdir $(SOURCES))))
DEPS := $(addprefix $(DEP_DIR)/, $(patsubst %.c, %.d, $(notdir $(SOURCES))))
# define VPATH
VPATH = $(LOCAL_PATH):$(LOCAL_PATH)/source_Dir1/:$(LOCAL_PATH)/source_Dir2/
# define the includes, compile and link flags
INCLUDES := -I$(LOCAL_PATH) -I$(LOCAL_PATH)/source_Dir1/ -I$(LOCAL_PATH)/source_Dir2/
CC_FLAGS := -g $(INCLUDES) 
LK_FLAGS := -L$(LOCAL_PATH)/lib_Dir
LK_FLAGS += -ltest -lm
# define the compiler
CC = gcc
# define the phony target
.PHONY : all clean
# build the target 
all: $(BUILD_DIR)/$(TARGET)
$(BUILD_DIR)/$(TARGET): $(OBJS)
	@if [ ! -d $(BUILD_DIR) ]; then mkdir -p $(BUILD_DIR); fi;\
	$(CC) $^ $(LK_FLAGS) -o $@
# build the objects
$(OBJ_DIR)/%.o : %.c
	@if [ ! -d $(OBJ_DIR) ]; then mkdir -p $(OBJ_DIR); fi;\
	$(CC) -c $(CC_FLAGS) -o $@ $<
# build the dependencies
$(DEP_DIR)/%.d : %.c
	@if [ ! -d $(DEP_DIR) ]; then mkdir -p $(DEP_DIR); fi;\
	set -e; rm -f $@;\
	$(CC) -MM $(CC_FLAGS) $< > $@.$$$$;\
	sed 's,\($*\)\.o[ :]*,$(OBJ_DIR)/\1.o $@ : ,g' < $@.$$$$ > $@;\
	rm -f $@.$$$$
# when *.h file changes, remake the project
-include $(DEPS)
# clean all products
clean:
	-rm -r $(BUILD_DIR)

2 Makefile的根本规矩

make是一个解释器,其会依据Makefile的内容,调用编译器等Linux指令,终究生成编译产品。Makefile的根本规矩如下:

target ... : prerequisites ...
	command
	...
	...
  • target:方针文件,能够是可重定位方针文件、可履行文件或动态库文件,还能够是标签(后续伪方针中会讲到);能够是一个或多个文件;
  • prerequisites:即生成target所需的文件,能够是一个或多个;
  • command:是make需履行的指令,前需求空一个制表符(Tab键);

以上描绘的是一个文件的依靠联系,target这方针文件依靠于prerequisites中的文件,生成规矩界说在command中。即当prerequisites中有一个以上的文件要比target中的文件要新的话,command指令就会被履行,这也是Makefile最中心的规矩:

  • 当咱们履行make指令时,它会首先找到文件中的第一个方针文件(target),在上面的比如里,第一个文件便是伪方针all,伪方针all代表了$(BUILD_DIR)/$(TARGET),依据变量界说,即test,也便是终究的可履行文件,后边会依据依靠联系找到生成方针文件所需的各级依靠文件;
  • test依靠于$(OBJS)(也便是.o)文件,.o文件依靠于源文件;在上述比如的第34-36行,test是方针文件,.o文件是依靠文件;在39-41行,.o文件方针文件,.c源文件是依靠文件;而是否从头生成方针文件取决于依靠文件是否要比方针文件新;在初次编译时(或履行完make clean后),依据依靠联系,.o文件不存在,即.c源文件的改动要比对应的.o新,那么会生成一切的.o文件,而test文件也不存在,那么一切生成的.o文件都会比test文件新,故而会生成test文件,那么终究结构可履行文件的进程也就完成了。在这以后,假如咱们修改了某一个所依靠的a.c文件,再次履行make指令时,会检查到a.c比a.o新,所以会生成a.o文件,这以后再检查到a.o比test要新,所以也会从头链接生成test文件。

也许有小伙伴会有疑问,为什么不运用gcc -o test *.c这种形式一步就生成可履行文件呢?原因在编译进程中,编译器会事先将每个源文件编译成可重定位方针文件(.o),然后链接器将一切的可重定位方针文件链接为可履行文件,那么依据以上规矩,每一个.c的改动都会完彻底全履行一遍一切的编译链接进程,而第1章比如中的写法能够在修改了某个模块后只编译此模块并从头链接到可履行文件,这在编译大型工程的时分有利于节约资源。

还有便是,上述比如中,只要33-41行的语法是无法做到头文件修改后,引证此头文件的源文件生成的模块从头编译的,因此需求44-52行的语法,这个后续会详细讲。
在大致了解了make在解析Makefile时分的作业机制后,后边我将一句第一章的比如涉及到的要素进行解说。

make在作业时的履行有以下过程(摘自陈皓的《跟我一同写Makefile》):

  • 1、将一切的Makefile和include包括的Makefile读入,包括include包括的依靠文件;
  • 2、初始化一切变量;
  • 3、推导隐晦规矩,并分析一切规矩,依据规矩为一切方针文件创立依靠联系链;
  • 4、依据依靠联系,确认哪些方针需求从头生成,并履行生成指令。

值得注意的是,针对变量,假如界说的变量被运用了,那么,make会把其打开在运用的位置。但make并不会彻底马上打开,make运用的是拖延战术,假如变量出现在依靠联系的规矩中,那么仅当这条依靠被决议要运用了,变量才会在其内部打开。

3 伪方针

咱们注意到在第33行,有.PHONY : all clean的界说,表明all和clean都是伪方针。伪方针并不是一个文件,仅仅一个标签,先然make不需求也无法依据依靠联系去生成这个标签,一般显式地经过关键字.PHONY去指明这是伪方针。
make履行的时分,假如不指定生成方针,会默许生成第一个可履行文件,假如需求生成多个可履行文件,可是不想敲过多的指令,能够界说一个名为all的的伪方针,指向几个可履行文件,这样直接履行make all的指令即可,本文比如中尽管只要一个可履行文件,可是仍是界说了all。

关于clean这个伪方针也是相同,咱们需求一个标签来删去生成的方针文件(包括中心产品),clean后不需求跟依靠文件,直接跟command指令即可。

4 变量

形如TARGET = test的形式是变量界说,在Makefile中界说的变量,就像是C言语中的宏相同,代表了一个文本字串,在Makefile中履行的时分会打开在所运用的地方,譬如在第33行,$(BUILD_DIR)/$(TARGET)就会被打开为build/test。不同于宏的是,变量能够在Makefile中改动值。在变量中,咱们运用$符号取变量值,在运用时,最好用“()”或“{}”将变量括起来,这样会更安全

变量的赋值符号除了“=”号还有几种,以下是它们的差异:

  • “=”:变量的值是整个Makefile中最后被指定的值;
  • “:=”:变量的值是当时位置的值;
  • “?=”:假如该变量没有被赋值,则赋予等号后的值;
  • “+=”:追加变量值,将等号后边的值添加到前面的变量上;

4.1 VPATH

VPATH是Makefile中的特别变量,不同于一般变量是用户自界说并在履行指令或解析依靠联系时再打开相同,VPATH是给Makefile用做寻觅文件的依靠联系时的途径。假如没有设置此变量,那么make只会在当时的目录中去寻觅依靠文件和方针文件;只要界说了这个变量,make会在当时目录找不到的状况下去指定的目录中寻觅。

另一个设置文件查找途径的办法是运用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很相似,可是它更为灵敏。它能够指定不同的文件在不同的查找目录中。这是一个很灵敏的功用。它的运用办法有三种:

  • 1、vpath <pattern> <directories>:为契合形式<pattern>的文件指定查找目录<directories>,<pattern>需求包括“%”通配符;
  • 2、vpath <pattern>:铲除契合形式<pattern>的文件的查找目录;
  • 3、vpath:铲除一切已被设置好了的文件查找目录;

4.2 隐含规矩下的变量

在隐含规矩下,根本会运用一些预先设置的变量,咱们既能够运用这些变量,也能够从头界说这些变量,在编译时,能够利用make的“-R”或“–no–builtin-variables”参数来撤销你所界说的变量对隐含规矩的效果。

4.2.1 指令相关的变量

  • AR:函数库打包程序,默许指令是“ar”;
  • AS:汇编言语编译程序,默许指令是“as”;
  • CC:C言语编译程序,默许指令是“cc”,这儿被重构为gcc,其实是一个指令:
$ ll /etc/alternatives/cc /usr/bin/cc
lrwxrwxrwx 1 root root 12 Nov 12  2014 /etc/alternatives/cc -> /usr/bin/gcc*
lrwxrwxrwx 1 root root 20 Nov 12  2014 /usr/bin/cc -> /etc/alternatives/cc*
  • RM:删去文件指令,默许是“rm -f”;
    还有许多就不一一列了。

4.2.2 指令参数的变量

  • CFLAGS:C言语编译器参数;
  • LDFLAGS:链接器参数;
  • ……

5 函数

在Makefile中运用函数来处理变量会使得指令更加的智能。函数调用的语法如下:

$(<function> <arguments>)

<function>指的是函数名,<arguments>指的是参数,参数间用“,”离隔,函数名和参数之间用空格离隔;函数调用以$符号开端,用“()”或“{}”将函数名和参数括起来,用法相似于变量,假如前面有赋值,则将会将函数回来值赋值给变量。如比如中的

LOCAL_PATH = $(shell pwd)
SOURCES := $(shell find $(LOCAL_PATH)/ -name "*.c")
OBJS := $(addprefix $(OBJ_DIR)/, $(patsubst %.c, %.o, $(notdir $(SOURCES))))
DEPS := $(addprefix $(DEP_DIR)/, $(patsubst %.c, %.d, $(notdir $(SOURCES))))

都用到了函数。下面就针对这几个函数讲讲,更多的函数界说还参考《跟我一同写Makefile》。

5.1 shell函数

shell函数的参数是操作体系的shell指令,如

LOCAL_PATH = $(shell pwd)
SOURCES := $(shell find $(LOCAL_PATH)/ -name "*.c")

第一行中,pwd指令会回来当时Makefile所在目录的途径,即将此途径指定为LOCAL_PATH,第二行中,表明在此途径及子途径下查找一切的源文件,并将其赋值给SOURCES。

5.2 addprefix函数

addprefix归于文件名操作函数,其根本格局如下,功用是把前缀<prefix>添加到<name>中的每个单词前面,并回来加过前缀的文件名序列。

$(addprefix <prefix>,<names...>)

5.3 patsubst函数

patsubst函数归于字符串处理函数,根本格局如下,功用是查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否契合形式<pattern>,假如匹配的话,则以<replacement>替换。这儿,<pattern>能够包括通配符“%”, 表明任意长度的字串。 假如<replacement>中也包括“%”, 那么, <replacement>中的这个“%”将是<pattern>中的那个“%”所代表的字串。(能够用“\”来转义, 以“%”来表明真实意义的“%”字符)。并回来被替换后的字符串。

$(patsubst <pattern>,<replacement>,<text>)

5.4 notdir函数

notdir函数用于取出文件称号中的非目录部分,并回来此部分,根本格局如下:

$(notdir <names...>)

所以结合以上三个函数的意义,就能解析以下句子的界说,即取出每个源文件替换成.o(.d)文件并添加上$(OBJ_DIR)$(DEP_DIR))的途径前缀,并回来给OBJSDEPS)变量。

OBJS := $(addprefix $(OBJ_DIR)/, $(patsubst %.c, %.o, $(notdir $(SOURCES))))
DEPS := $(addprefix $(DEP_DIR)/, $(patsubst %.c, %.d, $(notdir $(SOURCES))))

6 符号界说

6.1 通配符

在Makefile中,% 表明的是通配符,和Unix体系中的 * 通配符有着不同的意义,我在网上找到的描绘我觉得都不是很好了解,下面是我的了解:

  • % 是Makefile的规矩通配符,它会对后边的集合进行二次界说,当make预备生成test时,会发现其依靠于许多.o,当看到相似于%.o : %.c的句子时,会将前面所依靠的.o逐一打开,并将.c前的%替换为相应称号前缀的.c,如下:
%.o : %.c
	gcc -c $< -o $@
等价于
a.o : a.c
	gcc -c a.c -o a.o
b.o : b.c
	gcc -c b.c -o b.o
……
  • * 符号是Unix体系的通配符,表明一切。

6.2 特别符号

  • $@:方针的名字;
  • $<:第一个依靠方针;
  • $^:依靠方针集;
  • $?:依靠方针集中更新过的文件;
  • -command:疏忽当时指令行所遇到的过错;
  • @command:command指令将不会回显。

6.3 gcc选项

在以上比如中还有一些gcc的选项符号,譬如:

# -I 表明去以下途径寻觅头文件,一般头文件的查找在本目录以及编译器自带的目录下寻觅,这是指定私有头文件目录
INCLUDES := -I$(LOCAL_PATH) -I$(LOCAL_PATH)/source_Dir1/ -I$(LOCAL_PATH)/source_Dir2/
# -L 表明去此途径下寻觅链接库文件,一般规范库文件会在规范途径下,假如用到私有库,应指定 
LK_FLAGS := -L$(LOCAL_PATH)/lib_Dir
# -l 表明链接此库,ltest表明库称号为libtest.so或libtest.a,lm是libm.so,规范math库
LK_FLAGS += -ltest -lm

7 主动生成依靠性

源文件中包括了头文件,即源文件依靠于头文件,当工程较大时,一一写出依靠性是不合适的。大多数供给了“-M”选项,用于主动寻觅源文件包括的头文件,并生成依靠联系,可是GNU C的编译器中,“-M”选项会将规范头文件也包括进来,咱们运用“-MM”参数,只包括自界说的头文件,如下:

$ cc -M main.c
main.o: main.c /usr/include/stdio.h /usr/include/features.h \
 /usr/include/x86_64-linux-gnu/bits/predefs.h \
 /usr/include/x86_64-linux-gnu/sys/cdefs.h \
 /usr/include/x86_64-linux-gnu/bits/wordsize.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs.h \
 /usr/include/x86_64-linux-gnu/gnu/stubs-64.h \
 /usr/lib/gcc/x86_64-linux-gnu/4.4.7/include/stddef.h \
 /usr/include/x86_64-linux-gnu/bits/types.h \
 /usr/include/x86_64-linux-gnu/bits/typesizes.h /usr/include/libio.h \
 /usr/include/_G_config.h /usr/include/wchar.h \
 /usr/lib/gcc/x86_64-linux-gnu/4.4.7/include/stdarg.h \
 /usr/include/x86_64-linux-gnu/bits/stdio_lim.h \
 /usr/include/x86_64-linux-gnu/bits/sys_errlist.h list.h
$ cc -MM main.c 
main.o: main.c list.h

要让Makefile主动检测依靠头文件有些困难,不过GNU组织主张把编译器为每一个源文件的主动生成的依靠联系放到一个文件中,为每一个“name.c”的文件都生成一个“name.d”的Makefile文件,.d文件中就寄存对应.c文件的依靠联系。于是,咱们能够写出.c文件和.d文件的依靠联系,并让make主动更新或自成.d文件,并把其包括在咱们的主Makefile中,这样,咱们就能够主动化地生成每个文件的依靠联系了。以下句子表现了这个规矩。

$(DEP_DIR)/%.d : %.c
	@if [ ! -d $(DEP_DIR) ]; then mkdir -p $(DEP_DIR); fi;\
	set -e; rm -f $@;\
	$(CC) -MM $(CC_FLAGS) $< > $@.$$$$;\
	sed 's,\($*\)\.o[ :]*,$(OBJ_DIR)/\1.o $@ : ,g' < $@.$$$$ > $@;\
	rm -f $@.$$$$
# when *.h file changes, remake the project
-include $(DEPS)

这个规矩的意思是,一切的.d文件依靠于.c文件,rm -f $@的意思是删去一切的方针,也便是.d文件,第二行的意思是,为每个依靠文件$<,也便是.c文件生成依靠文件,$@表明形式%.d文件,假如有一个C文件是name.c,那么%便是name$$$$意为一个随机编号,第二行生成的文件有可能是name.d.12345,第三行运用sed指令做了一个替换,关于sed指令的用法请参看相关的运用文档。第四行便是删去临时文件。

以上句子能够确保每次生成新的依靠文件,要用include指令包括进Makefile,这样在头文件更新后,也会编译相应的方针。

8 总结

以上便是我关于Makefile的一些学习的笔记和总结,尽管没有针对Makefile进行十分详细且深化的研究,可是关于平时作业中关于Makefile的编写和阅读,应该也是满足应付的。想对Makefile的编写有更深化了解的,请移步陈皓大神的《跟我一同写Makefile》。