在下面这个例子中,生成一个名为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)
make是一个解释器,其会根据Makefile的内容,调用编译器等Linux命令,最终生成编译产物。Makefile的基本规则如下:
target ... : prerequisites ...
command
...
...
以上描述的是一个文件的依赖关系,target这目标文件依赖于prerequisites中的文件,生成规则定义在command中。即当prerequisites中有一个以上的文件要比target中的文件要新的话,command命令就会被执行,这也是Makefile最核心的规则:
也许有小伙伴会有疑问,为什么不使用gcc -o test *.c这种形式一步就生成可执行文件呢?原因在编译过程中,编译器会事先将每个源文件编译成可重定位目标文件(.o),然后链接器将所有的可重定位目标文件链接为可执行文件,那么根据以上规则,每一个.c的改动都会完完全全执行一遍所有的编译链接过程,而第1章例子中的写法可以在修改了某个模块后只编译此模块并重新链接到可执行文件,这在编译大型工程的时候有利于节省资源。
还有就是,上述例子中,只有33-41行的语法是无法做到头文件修改后,引用此头文件的源文件生成的模块重新编译的,因此需要44-52行的语法,这个后续会详细讲。
在大致了解了make在解析Makefile时候的工作机制后,后面我将一句第一章的例子涉及到的要素进行讲解。
make在工作时的执行有以下步骤:
值得注意的是,针对变量,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
我们注意到在第33行,有.PHONY : all clean的定义,表示all和clean都是伪目标。伪目标并不是一个文件,只是一个标签,先然make不需要也无法根据依赖关系去生成这个标签,一般显式地通过关键字.PHONY去指明这是伪目标。
make执行的时候,如果不指定生成目标,会默认生成第一个可执行文件,如果需要生成多个可执行文件,但是不想敲过多的命令,可以定义一个名为all的的伪目标,指向几个可执行文件,这样直接执行make all的指令即可,本文例子中虽然只有一个可执行文件,但是还是定义了all。
对于clean这个伪目标也是一样,我们需要一个标签来删除生成的目标文件(包括中间产物),clean后不需要跟依赖文件,直接跟command指令即可。
形如TARGET = test的形式是变量定义,在Makefile中定义的变量,就像是C语言中的宏一样,代表了一个文本字串,在Makefile中执行的时候会展开在所使用的地方,譬如在第33行,
(
B
U
I
L
D
D
I
R
)
/
(BUILD_DIR)/
(BUILDD?IR)/(TARGET)就会被展开为build/test。不同于宏的是,变量可以在Makefile中改变值。在变量中,我们使用$符号取变量值,在使用时,最好用“()”或“{}”将变量括起来,这样会更安全。
变量的赋值符号除了“=”号还有几种,以下是它们的区别:
VPATH是Makefile中的特殊变量,不同于一般变量是用户自定义并在执行命令或解析依赖关系时再展开一样,VPATH是给Makefile用做寻找文件的依赖关系时的路径。如果没有设置此变量,那么make只会在当前的目录中去寻找依赖文件和目标文件;只有定义了这个变量,make会在当前目录找不到的情况下去指定的目录中寻找。
另一个设置文件搜索路径的方法是使用make的“vpath”关键字(注意,它是全小写的),这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。这是一个很灵活的功能。它的使用方法有三种:
在隐含规则下,基本会使用一些预先设置的变量,我们既可以使用这些变量,也可以重新定义这些变量,在编译时,可以利用make的“-R”或“–no–builtin-variables”参数来取消你所定义的变量对隐含规则的作用。
$ 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*
在Makefile中使用函数来处理变量会使得命令更加的智能。函数调用的语法如下:
$(<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))))
都用到了函数。下面就针对这几个函数讲讲
shell函数的参数是操作系统的shell命令,如
LOCAL_PATH = $(shell pwd)
SOURCES := $(shell find $(LOCAL_PATH)/ -name "*.c")
第一行中,pwd指令会返回当前Makefile所在目录的路径,即将此路径指定为LOCAL_PATH,第二行中,表示在此路径及子路径下查找所有的源文件,并将其赋值给SOURCES。
addprefix属于文件名操作函数,其基本格式如下,功能是把前缀添加到中的每个单词前面,并返回加过前缀的文件名序列。
$(addprefix <prefix>,<names...>)
patsubst函数属于字符串处理函数,基本格式如下,功能是查找
$(patsubst <pattern>,<replacement>,<text>)
notdir函数用于取出文件名称中的非目录部分,并返回此部分,基本格式如下:
$(notdir <names...>)
所以结合以上三个函数的含义,就能解析以下语句的定义,即取出每个源文件替换成.o(.d)文件并添加上 ( O B J D I R ) ( (OBJ_DIR)( (OBJD?IR)((DEP_DIR))的路径前缀,并返回给OBJS(DEPS)变量。
OBJS := $(addprefix $(OBJ_DIR)/, $(patsubst %.c, %.o, $(notdir $(SOURCES))))
DEPS := $(addprefix $(DEP_DIR)/, $(patsubst %.c, %.d, $(notdir $(SOURCES))))
在Makefile中,% 表示的是通配符,和Unix系统中的 * 通配符有着不同的含义,我在网上找到的描述我觉得都不是很好理解,下面是我的理解:
%.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
……
在以上例子中还有一些gcc的选项符号,譬如:
bash复制代码
# -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
源文件中包含了头文件,即源文件依赖于头文件,当工程较大时,一一写出依赖性是不合适的。大多数提供了“-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
文件,第二行的意思是,为每个依赖文件
@的意思是删除所有的目标,也就是.d文件,第二行的意思是,为每个依赖文件
@的意思是删除所有的目标,也就是.d文件,第二行的意思是,为每个依赖文件<,也就是.c文件生成依赖文件,
@
表示模式
@表示模式%.d文件,如果有一个C文件是name.c,那么%就是name,
@表示模式$$$意为一个随机编号,第二行生成的文件有可能是name.d.12345,第三行使用sed命令做了一个替换,关于sed命令的用法请参看相关的使用文档。第四行就是删除临时文件。
以上语句可以保证每次生成新的依赖文件,要用include命令包含进Makefile,这样在头文件更新后,也会编译相应的目标。