本文记录学习Makefile的过程,从C工程编译入手,在看之前APUE TLPI等书时源代码中均采用了Makefile的形式,因此此篇文章记录学习过程,会长期更新,如果有任何错误与疑问欢迎指出。
在编译一个大型项目的时候,往往有很多目标文件、库文件、头文件以及最终的可执行文件。不同的文件之间存在依赖关系(dependency)。
在我们编译一个大型项目时,我们往往要很多次的调用编译器,来根据依赖关系,逐步编译整个项目。这样的方式是自下而上的,即先编译下游文件,再编译上游文件。
UNIX系统下的make工具用于自动记录和处理文件之间的依赖关系。我们不用输入大量的"gcc"命令,而只需调用make就可以完成整个编译过程。所有的依赖关系都记录在makefile、Makefile文本文件中。我们只需要输入make,make会根据依赖关系,自上而下的找到编译该文件所需的所有依赖关系,最后再自下而上的编译。
make有多个版本,本文将基于GNU make。make会自动搜索当前目录下的makefile, Makefile或者GNUmakefile。
下面我们以一个实例来演示,因为学习Makefile的终极目标是使用,且通过例子能够更好的理解。
参考如下示例代码:
fun.c

#include <stdio.h>

void print()
{
    printf("Hello,world.\n");
}

main.c

#include <stdio.h>

extern void print();

int main()
{
    print();
    return 0;
}

我们先来了解下gcc编译链接过程,使用gcc -c fun.c gcc -c main.c将分别生成对应的fun.o main.o文件,然后使用gcc -o main fun.o main.o将生成可执行文件main.
执行过程如下:

在上面的执行过程中,我们是先通过gcc -c生成了.o文件,然后有了.o文件才去生成可执行文件,这中间有一个依赖关系,要先有fun.o main.o,然后才能生成可执行文件main.
有了上面的认识过程我们再来看Makefile语法:
#号开头表示注释语句
target: prerequisite为依赖关系,即目标文件(target)依赖于前提文件(prerequisite)。可以有多个前提文件,用空格分开。
依赖关系后面的缩进行是实现依赖关系进行的操作,即正常的UNIX命令。一个依赖关系可以附属有多个操作。
上面的语法介绍可能比较抽象,我们通过前面解释知道Makefile是文本文件,因此vi Makefile输入如下内容:

# Makefile is a binary file
main:fun.o main.o
        gcc -o main fun.o main.o

fun.o: fun.c
        gcc -c  fun.c

main.o: main.c
        gcc -c main.c

上面的形式target:prerequisite形式通俗的讲是要生成main可执行文件前提是要有fun.o main.o,如果有了文件fun.o main.o的话再去执行下面的操作 gcc -o main fun.o main.o
如果没有fun.o 或main.o呢?
同样的道理,语句fun.o fun.c表示要生成fun.o我们必须有文件fun.c(这个我们真的有),然后执行gcc -c fun.c则生成fun.o
然后将上述文件命名为xxx,如果名字是Makefile makefile ma则直接输入make即可生成可执行文件main,如果上述文件命名成hello,则输入make hello即可.

# Makefile is a binary file
CFLAGS = -Wall -O -std=c99
CC = gcc

main:fun.o main.o
        ${CC} -o main fun.o main.o 

fun.o: fun.c
        ${CC} -c fun.c

main.o: main.c
        ${CC} -c main.c

与C语言中的宏一样,可以将常用的表示成宏,如果后续有修改,则只用修改一次,比如我对程序的编译方式不使用gcc,而是使用别的命令。
clean:
常用于清理历史文件。
比如在我们的例子中生成了fun.o 和main.o我们可以进行清理。

# Makefile is a binary file
CFLAGS = -Wall -O -std=c99
CC = gcc

main:fun.o main.o
        ${CC} -o main fun.o main.o 

fun.o: fun.c
        ${CC} -c fun.c

main.o: main.c
        ${CC} -c main.c
clean:
        rm *.o

输入make clean则会清除所有的.o文件。
假如我们本地恰好有个文件叫clean,而我们在Makefile中clean规则没有依赖,那么我们make clean时就不会执行上面的清理动作,如果改变这一问题呢?可以通过伪目标来实现:
伪目标是这样一个目标:它不代表一个真正的文件名,在执行make时可以指定这个目标来执行其所在规则定义的命令,有时我们也可以将一个伪目标称为标签。
使用伪目标有两点原因:

  1. 避免在我们的Makefile中定义的只执行命令的的目标(此目标的目的为了执行执行一系列命令,而不需要创建这个目标)和工作目录下的实际文件出现名字冲突。
  2. 提高执行make时的效率,特别是对于一个大型的工程来说,编译的效率也许你同样关心。

一般情况下,一个伪目标不作为一个另外一个目标文件的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则时伪目标所定义的命令都会被执行(因为它是规则的依赖,重建规则目标文件时需要首先重建它的依赖)。当伪目标没有作为任何目标(此目标是一个可被创建或者已存在的文件)的依赖时,我们只能通过make的命令行选项明确指定这个伪目标,来执行它所定义的命令。例如我们的make clean。

Makefile中,伪目标可以有自己的依赖。在一个目录下如果需要创建多个可执行程序,我们可以将所有程序的重建规则在一个Makefile中描述。因为Makefile中第一个目标是终极目标,约定的做法是使用一个称为all的伪目标来作为终极目标,它的依赖文件就是那些需要创建的程序。下边就是一个例子:

#sample Makefile 
all : prog1 prog2 prog3 
.PHONY : all 
prog1 : prog1.o utils.o 
cc -o prog1 prog1.o utils.o 
prog2 : prog2.o 
cc -o prog2 prog2.o 
prog3 : prog3.o sort.o utils.o 
cc -o prog3 prog3.o sort.o utils.o 

执行make时,目标all被作为终极目标。为了完成对它的更新,make会创建(不存在)或者重建(已存在)目标all的所有依赖文件(prog1、prog2和prog3)。当需要单独更新某一个程序时,我们可以通过make的命令行选项来明确指定需要重建的程序。(例如: make prog1)。 当一个伪目标作为另外一个伪目标依赖时,make将其作为另外一个伪目标的子例程来处理(可以这样理解:其作为另外一个伪目标的必须执行的部分,就行C语言中的函数调用一样)。下边的例子就是这种用法:

.PHONY: cleanall cleanobj cleandiff 
cleanall : cleanobj cleandiff 
rm program 
cleanobj : 
rm *.o 
cleandiff : 
rm *.diff

cleanobj和cleandiff这两个伪目标有点像子程序的意思(执行目标clearall时会触发它们所定义的命令被执行)。我们可以输入make cleanall和make cleanobj和make cleandiff命令来达到清除不同种类文件的目的。例子首先通过特殊目标.PHONY声明了多个伪目标,它们之间使用空各分割,之后才是各个伪目标的规则定义。

说明:
通常在清除文件的伪目标所定义的命令中rm使用选项–f(--force)来防止在缺少删除文件时出错并退出,使make clean过程失败。也可以在rm之前加上-来防止rm错误退出,这种方式时make会提示错误信息但不会退出。为了不看到这些讨厌的信息,需要使用上述的第一种方式。
另外make存在一个内嵌隐含变量RM,它被定义为:RM = rm –f。因此在书写clean规则的命令行时可以使用变量$(RM)来代替rm,这样可以免出现一些不必要的麻烦!这是我们推荐的用法。