Makefile 中 “嵌套执行make“ 大部分是在大的工程项目中使用的,那么我们就通过一个大的项目工程来详细的分析一下如何嵌套执行 make

    假设有一个 MP3 player 的应用程序,它可以被划分为若干个组件:用户界面(ui)、编解码器(codec)以及数据管理库(db)。它们分别可以用三个程序库来表示:libui.a、libcodec.a 和 libdb.a。

    将这些组件紧凑的放到一起就可以组成这个应用程序。具体的文件结构展示为(我们展示的只是目录文件,没有展示详细的源文件):

    1. ├──Makefile # 最外层的Makefile文件,不是目录文件。
    2. ├──lib # 编译的时候需要链接的库文件
    3. ├──codec # libui.a 库文件所在的目录
    4. ├──db # libdb.a 库文件所在的目录
    5. ├──ui # libui.a库文件所在的目录
    6. ├──src # 源文件所在的目录,子目录文件中包含Makefile文件
    7. ├──codec # 编解码器所在的源文件的目录
    8. ├──db # 数据库源文件所在的目录
    9. ├──ui # 用户界面源文件所在目录
    10. ├──app
    11. ├──player
    12. └──doc # 这个工程编译说明

    我们可以看到最外层有一个 Makefile 文件,这就是我们的 “总控Makefile” 文件,我们使用这个 Makefile 调用项目中各个子目录的 Makefile 文件的运行。假设只有我们的 src 目录下和 app 目录下的各个子目录含有 Makefile 文件。那我们总控的 Makefile 的文件可以这样来写:

    1. src_codec := src/codec
    2. src_db := src/db
    3. src_ui := src/ui
    4. libraries := $(src_codec) $(src_db) $(src_ui)
    5. player := app/player
    6. .PHONY : all $(player) $(libraries)
    7. all : $(player)
    8. $(player) $(libraries) :
    9. $(MAKE) -C $@

    我们可以看到在 “总控 Makefile” 中,一个规则在工作目标上列出了所有的子目录,它对每一个子目录的 Makefile 调用的代码是:

    1. $(player) $(libraries) :
    2. $(MAKE) -C $@

    在 Makefile 文件中,MAKE 变量应该总是用来调用 make 程序。make 程序一看到 MAKE 变量就会把它设成 make 的实际路径,所以递归调用中的每次调用都会使用同一个执行文件。此外,当命令 --touch(-t)--just-print(-n)--question(-q) 被使用时,包含 MAKE 变量的每一行都会受到特别的处理。

    由于这些“工作目标目录”被设成 .PHONY 的依赖文件,所以即使工作目标已经更新,此规则仍旧会进行更新动作。使 --directory(-C) 选项的目的是要让 make 在读取 Makefile 之前先切换到相应的 “工作目录” 。

    make 在建立依存图的时候找不到程序库与 app/player 工作目标之间的依存关系时,这意味着建立任何程序库之前,make 将会先执行 app/player 目录中的 Makefile。显然这将会导致失败的结果,因为应用程序的链接需要程序库。为解决这个问题,我们会提供额外的依存信息:

    1. $(player) : $(libraries)
    2. $(src_ui) : $(src_db) $(src_codec)

    我们在此处做了如下的描述:运行 app/player 目录中的 Makefile 之前必须先运行程序库子目录中的 Makefile。此外,编译 src/ui 目录中的程序代码之前必须先编译 src/db 和 src/codec 目录中的程序库。这么做可以确保任何自动产生的程序代码,在 src/ui 目录中的程序代码被编译之前就已经产生出来了。

    更新必要条件的时候,会引发微妙的次序问题。如同所有的依存关系,更新的次序取决于依存图的分析结果,但是当工作目标的必要条件(依赖文件)出现在同一行时,GNU make 将会从左至右的次序进行更新。例如:

    1. all : a b c
    2. all : d e f

    如果不存在其他的依存关系,这6个必要条件的更新动作可以是任何次序,不过GNU make将会以从左向右的次序来更新出现在同一行的必要条件,这会产生如下的更新次序:”a b c d e f” 或 “d e f a b c”。

    注意:不要因为之前这么做更新的次序是对的,就以为每次这么做都是对的,而忘了提供完整的依存信息。

    最后,依存分析可能会产生不同的次序而引发一些问题。所以,如果有一组工作目标需要以特定的次序进行更新时,就必须提供适当的必要条件来实现正确的次序。

    当我们在最外层执行 make 的时候我们会看到l输出的信息:

    1. make -C src/codec
    2. make[1]: Entering directory '/root/MP3_player/src/codec'
    3. gcc -o codec main.o ../../lib/codec/libcodec.a
    4. make[1]: Leaving directory '/root/MP3_player/src/codec'
    5. make -C src/db
    6. make[1]: Entering directory '/root/MP3_player/src/db'
    7. gcc -o db main.o db.o
    8. make[1]: Leaving directory '/root/MP3_player/src/db'
    9. make -C src/ui
    10. make[1]: Entering directory '/root/MP3_player/src/ui'
    11. gcc -o ui main.o ui.o
    12. make[1]: Leaving directory '/root/MP3_player/src/ui'
    13. make -C app/player
    14. make[1]: Entering directory '/root/MP3_player/app/player'
    15. gcc -o player main.o player.o
    16. make[1]: Leaving directory '/root/MP3_player/app/player'

    make 发觉它正在递归调用另一个 make 时,他会启 用--print-directory(-w)选项,这会使得 make 输出 Entering directory (进入目录) 和 Leaving directory (离开目录) 的信息。当 --directory(-C) 选项被使用时,也会启用这个选项。我们还可以看到每一行中,MAKELEVEL 这个 make 变量的值加上方括号之后被一起输出。在这个简单的例子里,每个组件的 Makefile 只会输出组件正在更新的信息,而不会真正的更新组件。

    我们通过这个例子应该可以了解,在 make 的嵌套执行执行的时候的调用子目录的方式,还有子目录再去执行 make 时候的顺序。这是一个很典型的例子,我们的每一个工程文件都可以用上面的结构展示出来,我们只要懂得每一个子目录在被调用时候的顺序,我们就可以很轻松的编写 “总控Makefile” 。

    本案例在目录下存在工程调试文件

    注意:附件中的的工程只能实现出我们案例中的现象,而并不是一个完整的 MP3_player 的应用程序。如果想要实现真正的音频播放功能,我们可以下载 mplayer 源码包,链接:http://www.mplayerhq.hu/MPlayer/releases/