Makefile简易教程

Makefile简介

在软件开发中,make通常被视为一种软件构建工具。该工具主要经由读取一种名为“makefile”或“Makefile”的文件来实现软件的自动化建构。它会通过一种被称之为“target”概念来检查相关文件之间的依赖关系,这种依赖关系的检查系统非常简单,主要通过对比文件的修改时间来实现。在大多数情况下,我们主要用它来编译源代码,生成结果代码,然后把结果代码连接起来生成可执行文件或者库文件。

编写一个最简单的Makefile

比如我们现在有main.cpp、func.cpp、func.h这么三个源代码文件。正常情况下我们是这样编译的(假设最后生成的目标程序叫prog):

1
g++ -o prog main.cpp func.cpp

如果没有makefile,在开发+调试程序的过程中,我们就需要不断地重复输入上面这条编译命令,要不就是通过终端的历史功能不停地按上下键来寻找最近执行过的命令。这样做两个缺陷:

  1. 一旦终端历史记录被丢失,我们就不得不从头开始;
  2. 任何时候只要我们修改了其中一个文件,上述编译命令就会重新编译所有的文件,当文件足够多时这样的编译会非常耗时。

那么Makefile又能做什么呢?我们先来看一个最简单的makefile文件:

1
2
prog: main.cpp func.cpp
g++ -o prog main.cpp func.cpp

以上就是一个最基本的Makefile语句,它主要分成了三个部分:第一行冒号之前的prog,我们称之为目标(target),被认为是这条语句所要处理的对象,具体到这里就是我们所要编译的这个程序calc。冒号后面的部分(main.cpp func.cpp),我们称之为依赖关系表,也就是编译calc所需要的文件,这些文件只要有一个发生了变化,就会触发该语句的第三部分,我们称其为命令部分,相信你也看得出这就是一条编译命令。现在我们只要将上面这两行语句写入一个名为Makefile或者makefile的文件,然后在终端中输入make命令,就会看到它按照我们的设定去编译程序了。

注意,在第二行的“g++”命令之前必须要有一个tab缩进。语法规定Makefile中的任何命令之前都必须要有一个tab缩进,否则make就会报错。

接下来,让我们来解决一下效率方面的问题,先初步修改一下上面的代码:

1
2
3
4
5
6
CXX = g++
TARGET = prog
SOURCE = main.cpp func.cpp

$(TARGET): $(SOURCE)
$(CXX) -o $(TARGET) $(SOURCE)

我们在上述代码中定义了三个常量CXX、TARGET以及SOURCE。它们分别告诉了make我们要使用的编译器、要编译的目标以及源文件。这样一来,今后我们要修改这三者中的任何一项,只需要修改常量的定义即可,而不用再去管后面的代码部分了。

但我们现在依然还是没能解决当我们只修改一个文件时就要全部重新编译的问题。而且如果我们修改的是func.h文件,make就无法察觉到变化了(所以有必要为头文件专门设置一个变量,并将其加入到依赖关系表中)。下面,我们来想一想如何解决这个问题。考虑到在标准的编译过程中,源文件往往是先被编译成目标文件,然后再由目标文件连接成可执行文件的。我们可以利用这一点来调整一下这些文件之间的依赖关系:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CXX = g++
TARGET = prog
DEPS = func.h
OBJ = main.o func.o

$(TARGET): $(SOURCE)
$(CXX) -o $(TARGET) $(SOURCE)

main.o: main.cpp $(DEPS)
$(CXX) -c main.cpp

func.o: func.cpp $(DEPS)
$(CXX) -c func.cpp

这样一来,上面的问题显然是解决了,但同时我们又让代码变得非常啰嗦,而且啰嗦往往伴随着低效率。经过再度观察,我们发现所有.cpp都会被编译成相同名称的.o文件。我们可以根据该特点再对其做进一步的简化:

1
2
3
4
5
6
7
8
9
10
CXX = g++
TARGET = prog
DEPS = func.h
OBJ = main.o func.o

$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)

%.o: %.c $(DEPS)
$(CXX) -c $< -o $@

在这里,我们用到了几个特殊的宏。首先是%.o:%.c,这是一个模式规则,表示所有的.o目标都依赖于与它同名的.c文件(当然还有DEPS中列出的头文件)。再来就是命令部分的$<和$@,其中$<代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用$^),具体到我们这里就是%.c。而$@代表的是当前语句的目标,即%.o。这样一来,make命令就会自动将所有的.c源文件编译成同名的.o文件。不用我们一项一项去指定了。整个代码自然简洁了许多。

到目前为止,我们已经有了一个不错的makefile,至少用来维护这个小型工程是没有什么问题了。当然,如果要进一步增加上面这个项目的可扩展性,我们就会需要用到一些Makefile中的伪目标和函数规则了。例如,如果我们想增加自动清理编译结果的功能就可以为其定义一个带伪目标的规则,在上述语句后面追加:

1
2
clean:
rm -rf $(OBJ) $(TARGET)

当我们在终端中执行make clean命令时,它就会去删除该工程生成的所有编译文件。

另外,如果我们需要往工程中添加一个.cpp或.h,可能同时就要再手动为obj常量再添加第一个.o文件,如果这列表很长,代码会非常难看,为此,我们需要用到Makefile中的函数,这里我们演示两个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CXX = g++
TARGET = prog
DEPS = $(shell find ./ -name "*.h")
SRC = $(shell find ./ -name "*.cpp")
OBJ = $(SRC:%.cpp=%.o)

$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(OBJ)

%.o: %.c $(DEPS)
$(CXX) -c $< -o $@

clean:
rm -rf $(OBJ) $(TARGET)

其中,shell函数主要用于执行shell命令,具体到这里就是找出当前目录下所有的.cpp和.h文件。而$(src:%.cpp=%.o)则是一个字符替换函数,它会将src所有的.cpp字串替换成.o,实际上就等于列出了所有.c文件要编译的结果。有了这两个设定,无论我们今后在该工程加入多少.c和.h文件,Makefile都能自动将其纳入到工程中来。

Makefile中引入第三方库写法

上面仅限于我们自己编写几个文件时的小工程使用,日常工作学习当中我们必然要使用大量第三方库,毕竟github上有着众多优质轮子,可以选择自己需要的来使用。

比如上一篇文章就说到了yaml-cpp,那么我们这次使用这个库作为演示。

假设在各个那个背景下,我们需要引入yaml-cpp库。那么我们知道,引入一个库可以使用静态库或者动态库(后面有机会可以总结下两者的区别)。这里使用动态库,在win平台下是.dll文件,linux下是.so文件。以及我们还需要include这个库的头文件(若干个)。那么我们在Makefile文件中加入这两个目录

1
2
3
ROOT_DIR := ../../
INC_DIR := $(ROOT_DIR)thirdlib/include
LIB_DIR := $(ROOT_DIR)thirdlib/lib

INC_DIR和LIB_DIR分别代表存放yaml的头文件和.so文件的目录(ROOT_DIR是整个项目根目录,每个人放工程目录不一样,理解即可)。那么我们把他们加入到我们的编译选项当中,如下

1
2
3
4
5
6
7
LD_LIBS := yaml-cpp
CXXFLAGS := -ggdb
CXXFLAGS += $(addprefix -I, $(INC_DIR))
LDFLAGS := $(addprefix -L,$(LIB_DIR)) $(addprefix -l,$(LD_LIBS))

$(TARGET): $(OBJ)
$(CXX) -o $(TARGET) $(CXXFLAGS) $(OBJ) $(LDFLAGS)

我们看到最后的编译命令在obj前面加了CXXFLAGS,也就是c++编译选项,这里就有刚刚的INC_DIR(如果有其他需要括进去的头文件目录也可以加进去,-ggdb是代表生成可供gdb是用的信息,使用-g也是类似的效果,但两者有细微的差别,一般更倾向于使用前者也就是-ggdb)。

LDFLAGS代表我们需要引入的库名,其中有两个命令参数,**-L** 指定库的路径 -l 指定需连接的库名。也就是上面的$(ROOT_DIR)thirdlib/include和yaml-cpp。

这样我们能在工程下任意文件里调用yaml-cpp的内容了,调用时加上#include "yaml-cpp/yaml.h"即可。

以上就是在工程中引入yaml-cpp需要对Makefile文件进行修改的简单总结了,后续这篇文章可能还会更新一些更细节的内容。诸如Makefile更优化的写法,gcc一些命令参数的对比或者备忘等等。

参考文章

gcc -I -L -l区别

Makefile简易教程

中文维基百科