makefile(1):初识makefile

274 阅读9分钟

0. 概述

什么是makefile?
makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令。

或许很多Winodws程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作。

1. 关于程序的编译和链接

一般来说,首先要把源文件编译成中间目标文件,在Windows下也就是 .obj 文件,UNIX下是 .o 文件,即 Object File,这个动作叫做编译(compile)。然后再把大量的Object File合成执行文件,这个动作叫作链接(link)。

编译时,编译器需要的是语法的正确,函数与变量的声明的正确。对于后者,通常是你需要告诉编译器头文件的所在位置,只要所有的语法正确,编译器就可以编译出中间目标文件。一般来说,每个源文件都应该对应于一个中间目标文件。

链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序。链接器并不管函数所在的源文件,只管函数的中间目标文件,在大多数时候,由于源文件太多,编译生成的中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件。

总结一下,源文件首先会生成中间目标文件,再由中间目标文件生成执行文件。在编译时,编译器只检测程序语法,和函数、变量是否被声明。如果函数未被声明,编译器会给出一个警告,但可以生成Object File。而在链接程序时,链接器会在所有的Object File中找寻函数的实现,如果找不到,那到就会报链接错误码。

2. Makefile 介绍

make命令执行时,需要一个 Makefile 文件,以告诉make命令需要怎么样的去编译和链接程序。

首先,我们用一个示例来说明Makefile的书写规则。在这个示例中,工程有8个C文件,和3个头文件,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

  1. 如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。
  2. 如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。
  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

2.1 Makefile的规则

在讲述这个Makefile之前,还是让我们先来粗略地看一看Makefile的规则。

    target ... : prerequisites ...
            command
            ...
            ...

    target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(Label),对于标签这种特性,在后续的“伪目标”章节中会有叙述。

    prerequisites就是,要生成那个target所需要的文件或是目标。

    command也就是make需要执行的命令。(任意的Shell命令)

这是一个文件的依赖关系,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。

2.2 一个示例

正如前面所说的,如果一个工程有3个头文件,和8个C文件,我们为了完成前面所述的那三个规则,我们的Makefile应该是下面的这个样子的。

    edit : main.o kbd.o command.o display.o /
           insert.o search.o files.o utils.o
            cc -o edit main.o kbd.o command.o display.o /
                       insert.o search.o files.o utils.o

    main.o : main.c defs.h
            cc -c main.c
    kbd.o : kbd.c defs.h command.h
            cc -c kbd.c
    command.o : command.c defs.h command.h
            cc -c command.c
    display.o : display.c defs.h buffer.h
            cc -c display.c
    insert.o : insert.c defs.h buffer.h
            cc -c insert.c
    search.o : search.c defs.h buffer.h
            cc -c search.c
    files.o : files.c defs.h buffer.h command.h
            cc -c files.c
    utils.o : utils.c defs.h
            cc -c utils.c
    clean :
            rm edit main.o kbd.o command.o display.o /
               insert.o search.o files.o utils.o

反斜杠(/)是换行符的意思。这样便于Makefile的易读。我们可以把内容保存在文件为“Makefile”或“makefile”的文件中,然后在该目录下直接输入命令“make”就可以生成执行文件edit。如果要删除执行文件和所有的中间目标文件,那么,只要执行“make clean”就可以了。

在这个makefile中,目标文件包含:执行文件edit和中间目标文件(*.o),依赖文件就是冒号后面的那些 .c 文件和 .h文件。每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质上就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。

定义好依赖关系后,后续一行定义如何生成目标文件的操作系统命令,以Tab键开头。make会比较目标文件和依赖文件的修改日期,如果依赖文件的日期要比目标文件的新,或者目标文件不存在,那么,make就会执行后续定义的命令。

clean是一个动作名字,冒号后什么也没有,make不会自动去找文件的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个lable的名字。这样的方法非常有用,我们可以在一个makefile中定义不用的编译或是和编译无关的命令,比如程序的打包,程序的备份,等等。

2.3 make是如何工作的

在默认的方式下,也就是我们只输入make命令。那么,

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的第一个目标文件,在上面的例子中找到“edit”这个文件作为最终的目标文件。
  3. 如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比它新,那么就会执行后面所定义的命令来生成edit这个文件。
  4. 如果edit所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,找到则根据那一个规则生成.o文件。
  5. 当然,你的C文件和H文件是存在的,于是make会生成 .o 文件,再用 .o 文件生成可执行文件edit

make一层层地去找文件的依赖关系,直到编译出第一个目标文件。在找寻的过程中如果出现错误,比如最后被依赖的文件找不到,make会直接退出并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。即我通过依赖关系执行命令后依赖文件还是无法生成则我就不管了

通过上述分析,我们知道像clean这种没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过,我们可以显示地要make执行。即命令——“make clean”,以此来清除所有的目标文件,以便重编译。

在编程中,如果工程已被编译过,当修改其中一个源文件如file.c,那么根据依赖性,file.o会被重编译,于是file.o的文件修改时间要比edit要新,所以edit也会被重新链接了。

2.4 makefile中使用变量

在上面的例子中,先让我们看看edit的规则:

  edit : main.o kbd.o command.o display.o /
              insert.o search.o files.o utils.o
        cc -o edit main.o kbd.o command.o display.o /
                   insert.o search.o files.o utils.o

[.o]文件的字符串重复了两次,如果工程需要加入一个新的[.o]文件,那么需要在三个地方加(还有一个地方在clean中)。如果makefile变得复杂,那么就可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中可以使用变量。makefile的变量就是一个字符串。

比如,我们声明一个变量,叫objects。我们在makefile一开始就这样定义:

 objects = main.o kbd.o command.o display.o /
          insert.o search.o files.o utils.o

我们可以很方便地在makefile中以“$(objects)”的方式使用这个变量:

    objects = main.o kbd.o command.o display.o /
              insert.o search.o files.o utils.o

    edit : $(objects)
            cc -o edit $(objects)
    ...
    clean :
            rm edit $(objects)

如果有新的 .o 文件加入,只需简单地修改一下 objects 变量即可。

2.5 让make自动推导

make可以自动推导文件以及文件依赖关系后面的命令,没必要在每一个[.o]文件后都写上类似的命令,make会自动识别并推导命令。

只要make看到一个[.o]文件,它就会自动把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂:

    objects = main.o kbd.o command.o display.o /
              insert.o search.o files.o utils.o

    edit : $(objects)
            cc -o edit $(objects)

    main.o : defs.h
    kbd.o : defs.h command.h
    command.o : defs.h command.h
    display.o : defs.h buffer.h
    insert.o : defs.h buffer.h
    search.o : defs.h buffer.h
    files.o : defs.h buffer.h command.h
    utils.o : defs.h

    .PHONY : clean
    clean :
            rm edit $(objects)

这种方法是make的“隐晦规则”。上面文件内容中.PHONY表示,clean是个伪目标文件。

2.6 另类风格的makefile

既然make可以自动推导命令,那么我看到那堆[.o]和[.h]的依赖就有点不爽,那么多的重复的[.h],能不能把其收拢起来?没有问题:

    objects = main.o kbd.o command.o display.o /
              insert.o search.o files.o utils.o

    edit : $(objects)
            cc -o edit $(objects)

    $(objects) : defs.h
    kbd.o command.o files.o : command.h
    display.o insert.o search.o files.o : buffer.h

    .PHONY : clean
    clean :
            rm edit $(objects)

这种风格,让我们的makefile变得很简单,但我们的文件依赖关系就显得有点凌乱了。鱼和熊掌不可兼得。还看你的喜好了。我是不喜欢这种风格的,一是文件的依赖关系看不清楚,二是如果文件一多,要加入几个新的.o文件,那就理不清楚了。

2.7 清空目标文件的规则

每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。

        clean:
            rm edit $(objects)

更为稳健的做法是:

        .PHONY : clean
        clean :
                -rm edit $(objects)

.PHONY意思表示clean是一个“伪目标”,。而在rm命令前加一个减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标。