Makefile文件教程(四)

十七、Makefile嵌套执行make

这个章节主要讲解的是在 Makefile 中嵌套执行 make。我们都知道在一个大的工程文件中,不同的文件按照功能被划分到不同的模块中,也就说很多的源文件被放置在了不同的目录下。每个模块可能都会有自己的编译顺序和规则,如果在一个 Makefile 文件中描述所有模块的编译规则,就会很乱,执行时也会不方便,所以就需要在不同的模块中分别对它们的规则进行描述,也就是每一个模块都编写一个 Makefile 文件,这样不仅方便管理,而且可以迅速发现模块中的问题。这样我们只需要控制其他模块中的 Makefile 就可以实现总体的控制,这就是 make 的嵌套执行。

如何来使用呢?举例说明如下:

  1. subsystem:
  2. cd subdir && $(MAKE)

这个例子可以这样来理解,在当前目录下有一个目录文件 subdir 和一个 Makefile 文件,子目录 subdir 文件下还有一个 Makefile 文件,这个文件是用来描述这个子目录文件的编译规则。使用时只需要在最外层的目录中执行 make 命令,当命令执行到上述的规则时,程序会进入到子目录中执行 make。这就是嵌套执行 make,我们把最外层的 Makefile 称为是总控 Makefile。

上述的规则也可以换成另外一种写法:

  1. subsystem
  2. $(MAKE) -C subdir

在 make 的嵌套执行中,我们需要了解一个变量 "CURDIR",此变量代表 make 的工作目录。当使用 make 的选项 "-C" 的时候,命令就会进入指定的目录中,然后此变量就会被重新赋值。总之,如果在 Makefile 中没有对此变量进行显式的赋值操作,那么它就表示 make 的工作目录。我们也可以在 Makefile 中为这个变量赋一个新的值,当然重新赋值后这个变量将不再代表 make 的工作目录。

export的使用

使用 make 嵌套执行的时候,变量是否传递也是我们需要注意的。如果需要变量的传递,那么可以这样来使用:

export <variable>

如果不需要那么可以这样来写:

unexport <variable>

<variable>是变量的名字,不需要使用 "$" 这个字符。如果所有的变量都需要传递,那么只需要使用 "export" 就可以,不需要添加变量的名字。

Makefile 中还有两个变量不管是不是使用关键字 "export" 声明,它们总会传递到下层的 Makefile 中。这两个变量分别是 SHELL 和 MAKEFLAGS,特别是 MAKEFLAGS 变量,包含了 make 的参数信息。如果执行总控 Makefile 时,make 命令带有参数或者在上层的 Makefile 中定义了这个变量,那么 MAKEFLAGS 变量的值将会是 make 命令传递的参数,并且会传递到下层的 Makefile 中,这是一个系统级别的环境变量。

make 命令中有几个参数选项并不传递,它们是:"-C"、"-f"、"-o"、"-h" 和 "-W"。如果我们不想传递 MAKEFLAGS 变量的值,在 Makefile 中可以这样来写:

  1. subsystem:
  2. cd subdir && $(MAKE) MAKEFLAGS=

十八、嵌套执行make的案例

Makefile 中 "嵌套执行 make" 大部分是在大的工程项目中使用的,那么我们就通过一个大的项目工程来详细的分析一下如何嵌套执行 make。
 
假设有一个 MP3 player 的应用程序,它可以被划分为若干个组件:用户界面(ui)、编解码器(codec)以及数据管理库(db)。它们分别可以用三个程序库来表示:libui.a、libcodec.a 和 libdb.a。将这些组件紧凑的放到一起就可以组成这个应用程序。具体的文件结构展示为(我们展示的只是目录文件,没有展示详细的源文件):

├──Makefile         //最外层的Makefile文件,不是目录文件。
├──include          //编译的时候需要链接的库文件
│      ├──codec   //libui.a 库文件所在的目录
│      ├──db        //libdb.a 库文件所在的目录
│      ├──ui         //libui.a库文件所在的目录

├──lib                   //源文件所在的目录,子目录文件中包含Makefile文件
│      ├──codec     //编解码器所在的源文件的目录
│      ├──db           //数据库源文件所在的目录
│      ├──ui            //用户界面源文件所在目录
├──app
│      ├──player    
└──doc              //这个工程编译说明    

我们可以看到最外层有一个 Makefile 文件,这就是我们的 "总控Makefile" 文件,我们使用这个 Makefile 调用项目中各个子目录的 Makefile 文件的运行。假设只有我们的 lib 目录下和 app 目录下的各个子目录含有 Makefile 文件。那我们总控的 Makefile 的文件可以这样来写:
 
  1. lib_codec := lib/codec
  2. lib_db := lib/db
  3. lib_ui := lib/ui
  4. libraries := $(lib_codec) $(lib_db) $(lib_ui)
  5. player := app/player
  6. .PHONY : all $(player) $(libraries)
  7. all : $(player)
  8. $(player) $(libraries) :
  9. $(MAKE) -C $@
我们可以看到在 "总控 Makefile" 中,一个规则在工作目标上列出了所有的子目录,它对每一个子目录的 Makefile 调用的代码是:

$(player) $(libraries) :
      $(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。显然这将会导致失败的结果,因为应用程序的链接需要程序库。为解决这个问题,我们会提供额外的依存信息:

$(player) : $(libraries)
$(lib_ui) : $(lib_db) $(lib_codec)

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

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

all : a b c
all : d e f

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

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

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

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

make -C lib/db
make[1]: Entering directory ‘/MP3_player/lib/db’
make[1]:Update db library...
make[1]: Leaving directory ‘/MP3_player/lib/db’
make -C lib/codec
make[1]: Entering directory ‘/MP3_player/lib/codec’
make[1]:Update codec library...
make[1]: Leaving directory ‘/MP3_player/lib/codec’
make -C lib/ui
make[1]: Entering directory ‘/MP3_player/lib/ui’
make[1]:Update ui library...
make[1]: Leaving directory ‘/MP3_player/lib/ui’
make -C app/player
make[1]: Entering directory ‘/MP3_player/app/player’
make[1]:Update player library...
make[1]: Leaving directory ‘/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/

 

猜你喜欢

转载自blog.csdn.net/qq_43590614/article/details/114332578