6、GNU makefile工程管理学习的一个例子

 

  在之前我们已经学习了一个文件的编译过程,但是做过项目的都知道,一个工程中的源文件不计其数,其按类型、功能、模块会分别放在若干个目录中,而这些文件如何编译就需要有一个编译规则,虽然现在很多大型的项目都是直接用诸如CMake、Autotools、SCons等工具生成的,但是对于一些小项目,使用工具就没有什么必要了,而且对于项目编写来说,掌握Makefile的基本知识对我们来说也是很有必要的。

  我们本篇文章所要讲解的就是这个编译规则的文件makefile的规则及如何进行编写。makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为 makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。

1.0从一个简单的例子开始

   这里,我们用一个示例来说明Makefile的书写规则。以便给大家一个感兴认识。这个示例来源于GNU的make使用手册,在这个示例中,在这个例子中,我们会看到一份主程序代码(main.c)、三份函数代码(getop.c、stack.c、getch.c)以及一个头文件(calc.h)。通常情况下,我们需要这样编译它:

gcc -o calc main.c getch.c getop.c stack.c 

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

  1. 一旦终端历史记录被丢失,我们就不得不从头开始;

  2. 任何时候只要我们修改了其中一个文件,上述编译命令就会重新编译所有的文件,当文件足够多时这样的编译会非常耗时。

为了减少麻烦和节约时间,我们要写一个Makefile来告诉make命令如何编译和链接这几个文件。我们的规则是:

      1.如果这个工程没有编译过,那么我们的所有C文件都要编译并被链接。

             2.如果这个工程的某几个C文件被修改,那么我们只编译被修改的C文件,并链接目标程序。

            3.如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的C文件,并链接目标程序。

       只要我们的Makefile写得够好,所有的这一切,我们只用一个make命令就可以完成,make命令会自动智能地根据当前的文件修改的情况来确定哪些文件需要重编译,从而自己编译所需要的文件和链接目标程序。

1.1 Makefile的规则

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

 target也就是一个目标文件,可以是Object File,也可以是执行文件。

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

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

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

通过了解规则,我们就掌握了makefile的核心内容,但是仅仅靠这个是无法自己编写好一个完美的makefile文件的。

这里我们看回到我们之前的示例,按照这个规则我们可以写出如下的一个简单的makefile文件:

   calc: main.c getch.c getop.c stack.c
        gcc -o calc main.c getch.c getop.c stack.c 

我么可以看到它主要分成了三个部分,第一行冒号之前的calc,就是我们所要编译的这个程序calc。冒号后面的部分(main.c getch.c getop.c stack.c),就是我们称之为依赖关系表,也就是编译calc所需要的文件,这些文件只要有一个发生了变化,就会触发该语句的第三部分,我们称其为命令部分。

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

这样,一个可以进行编译的makefile文件写出来了,但是,我们可以看出,整个makefile文件比较基础,没有进行过优化,这样,在编译比较大且文件多的工程的时候效率是远远达不到的,所以我们需要通过下面的学习来进行重构,以提高整个makefile的编译效率。

1.3makefile中使用变量

有时候我们会需要在makefile中增添文件,我们这个makefile显然可以直接在依赖链和指令中直接添加文件名来完成,但是当文件较大时想要直接添加可能就比较复杂和耗费时间了,所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。

    cc = gcc
    prom = calc
    src = main.c getch.c getop.c stack.c
     
    $(prom): $(src)
        $(cc) -o $(prom) $(src)
如你所见,我们在上述代码中定义了三个常量cc、prom以及src。它们分别告诉了make我们要使用的编译器、要编译的目标以及源文件。这样一来,今后我们要修改这三者中的任何一项,只需要修改常量的定义即可,而不用再去管后面的代码部分了。

1.4、make是如何工作的

  之前进行过优化后,我们解决了增添文件的问题,但我们现在依然还是没能解决当我们只修改一个文件时就要全部重新编译的问题。而且如果我们修改的是calc.h文件,make就无法察觉到变化了(所以有必要为头文件专门设置一个常量,并将其加入到依赖关系表中)。下面,我们需要通过了解make是如何进行工作的,来对整个makefile文件进行优化。在默认的方式下,也就是我们只输入make命令。那么,
1、make会在当前目录下找名字叫“Makefile”或“makefile”的文件。当名字不为这俩着之一时,可以用以下方法指定。
make f 文件名

2、如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“calc”这个文件,并把这个文件作为最终的目标文件。
3、如果calc文件不存在,或是calc所依赖的后面的 .o 文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。
4、如果calc所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
5、当然,你的C文件和H文件是存在的啦,于是make会生成 .o 文件,然后再用 .o 文件生命make的终极任务,也就是执行文件calc了。
通过了解make是如何工作的,我们知道可以在标准的编译过程中,源文件往往是先被编译成目标文件,然后再由目标文件连接成可执行文件的。我们可以利用这一点来调整一下这些文件之间的依赖关系:
    cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    main.o: main.c $(deps)
        $(cc) -c main.c

    getch.o: getch.c $(deps)
        $(cc) -c getch.c

    getop.o: getop.c $(deps)
        $(cc) -c getop.c

    stack.o: stack.c $(deps)
        $(cc) -c stack.c

这样一来,上面的问题显然是解决了,但是现在makefile文件又变的啰嗦了,这里还是可以用变量的方法来对其进行简化

    cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@
  在这里,你可能看不懂最后两行的命令的意思,其实他们是makefile中存在的系统默认的自动化变量。首先是%.o:%.c,这是一个模式规则,表示所有的.o目标都依赖于与它同名的.c文件(当然还有deps中列出的头文件)。再来就是命令部分的$<和$@,其中$<代表的是依赖关系表中的第一项(如果我们想引用的是整个关系表,那么就应该使用$^),具体到我们这里就是%.c。而$@代表的是当前语句的目标,即%.o。这样一来,make命令就会自动将所有的.c源文件编译成同名的.o文件。不用我们一项一项去指定了。
  到目前为止,我们已经有了一个不错的makefile,至少用来维护这个小型工程是没有什么问题了。

1.5清空目标文件的规则

  每个Makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。一般的风格都是:
cc = gcc
    prom = calc
    deps = calc.h
    obj = main.o getch.o getop.o stack.o
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@
.PHONY:clean
    clean:
        rm -rf $(obj) $(prom)

有了上面最后两行代码,当我们在终端中执行make clean命令时,它就会去删除该工程生成的所有编译文件,从而保持整个工程整洁。.PHONY意思表示clean是一个“伪目标”,。而在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

1.6使用函数使得makefile更智能

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

cc = gcc
    prom = calc
    deps = $(shell find ./ -name "*.h")
    src = $(shell find ./ -name "*.c")
    obj = $(src:%.c=%.o) 
    
    $(prom): $(obj)
        $(cc) -o $(prom) $(obj)

    %.o: %.c $(deps)
        $(cc) -c $< -o $@

    clean:
        rm -rf $(obj) $(prom)
其中,shell函数主要用于执行shell命令,具体到这里就是找出当前目录下所有的.c和.h文件。而$(src:%.c=%.o)则是一个字符替换函数,它会将src所有的.c字串替换成.o,实际上就等于列出了所有.c文件要编译的结果。有了这两个设定,无论我们今后在该工程加入多少.c和.h文件,Makefile都能自动将其纳入到工程中来。
到这里,整个makefile文件的编写例程就介绍完毕了,你也可以自己尝试着写一个简单的makefile文件进行编译学习。

推荐书籍下载链接

http://download.csdn.net/download/noticeable/10242106

Makefile工具下载

CMake - Cross Platform Make

Automake - GNU Project

SCons: A software construction tool

https://www.lrde.epita.fr/~adl/autotools.html

 

参考资料

http://blog.csdn.net/ruglcc/article/details/7814546

https://www.gnu.org/software/make/manual/make.html

https://www.zhihu.com/question/23792247

https://yq.aliyun.com/articles/9243

http://blog.csdn.net/liang13664759/article/details/1771246

http://blog.csdn.net/haoel/article/details/2886/

https://www.jianshu.com/p/ff0e0e26c47a

posted @ 2018-02-06 13:29  noticeable  阅读(252)  评论(0编辑  收藏  举报