Linux 中的 Makefiles: 概述






4.95/5 (83投票s)
解释 make 命令的用法和 makefile 的语法。
引言
包含几个模块的小型 C/C++ 应用程序易于管理。开发人员可以通过直接调用编译器,并将源文件作为参数传递来轻松重新编译它们。这是一种简单的方法。然而,当项目变得过于复杂,包含许多源文件时,就需要一个工具来帮助开发人员管理项目。
我所说的工具是 make 命令。make 命令不仅用于帮助开发人员编译应用程序,还可以用于任何您想从多个输入文件生成输出文件的情况。
本文不是一个完整的教程,它侧重于 C 应用程序以及如何使用 make 命令和 makefile 来构建它们。有一个 zip 文件,其中包含许多按目录结构组织的示例。示例中最重要的文件是 makefile,而不是 C 源代码。您应该下载示例文件,并使用 unzip 命令或任何其他首选工具解压缩。因此,为了更好地理解本文,请
- 下载 make_samples.zip。
- 打开终端会话。
- 在您的主目录中创建一个目录。
- 将 make_samples.zip 移动到创建的目录中。
- 解压缩:unzip make_samples.zip。
目录
- 1. Make 工具:语法概述
- 2. Makefiles 的基本语法
- 3. 假目标、宏和特殊字符
- 4. 后缀规则:简化 Makefiles 语法
- 5. 一个调用其他 Makefiles 的 Makefile
- 6. 结论
. Make 工具:语法概述
make 命令语法是
make [选项] [目标]
您可以输入 make --help 来查看 make 命令支持的所有选项。在本文中,解释所有这些选项不在范围之内。重点是 makefile 结构及其工作原理。target 是 makefile 中存在的一个标签(或定义的名称)。它将在本文后面描述。
make 需要一个 makefile 来告诉它您的应用程序应该如何构建。makefile 通常与源文件位于同一目录中,它可以有任何您想要的名称。例如,如果您的 makefile 名为 run.mk,那么要执行 make 命令,请键入
make -f run.mk
-f 选项告诉 make 命令应该处理的 makefile 名称。
还有两个特殊名称使得 -f 选项不是必需的:makefile 和 Makefile。如果您运行 make 而不传递文件名,它将首先查找名为 makefile 的文件。如果不存在,它将查找名为 Makefile 的文件。如果您的目录中有两个文件,一个名为 makefile,另一个名为 Makefile,并且您输入
make <enter>
make 命令将处理名为 makefile 的文件。在这种情况下,如果您想让 make 命令处理 Makefile,您应该使用 -f 选项。
2. Makefiles 的基本语法
一个 make 文件由一组 目标、依赖项 和 规则 组成。目标 大多数时候是要创建/更新的文件。目标 依赖于 依赖项列表 中描述的一组源文件甚至其他 目标。规则 是使用 依赖项列表 创建 目标 文件所需的命令。
如 图 1 所示,规则 部分中的每个命令都必须在以 TAB 字符开头的行上。空间问题会导致错误。此外,规则 行末尾的空格可能会导致 make 发出错误消息。
makefile 由 make 命令读取,该命令通过比较 依赖项列表 中源文件的时间戳来确定要构建的 目标 文件。如果自上次构建以来任何依赖项的时间戳已更改,make 命令将执行与 目标 关联的规则。
2.1 测试 sample1
现在是进行一个简单测试的时候了。源代码包含一个名为 sample1 的目录。其中有四个文件:
- app.c 和 inc_a.h:一个简单的 C 应用程序。
- mkfile.r:一个正确的 makefile(.r 扩展名表示正确)。
- mkfile.w:一个不完整或写得不好的 makefile(.w 扩展名表示错误)。这不意味着存在语法错误。
所以,我们有
mkfile.r
# This is a very simple makefile
app: app.c inc_a.h
cc -o app app.c
和
mkfile.w
# This is a very simple makefile
app: app.c
cc -o app app.c
它们之间的差异看起来似乎无关紧要,但在某些情况下是相关的。首先,让我们检查 makefile 的部分
- 字符 # 用于在 makefile 中插入注释。从注释字符到行尾的所有文本都将被忽略。
- app 是 目标,即必须构建的可执行文件。
- app.c 和 inc_a.h 是 目标 app 的 依赖项(inc_a.h 仅存在于 mkfile.r 中)。
- cc -o app app.c 是用于构建 目标 的 规则,它会考虑 依赖项列表 中文件的任何更改。
为了演示它们的工作原理,让我们尝试以下命令序列(图 2)
- 调用 make 命令来处理 mkfile.r,您可以看到正在执行 规则 以创建 app 可执行文件。
- app 被删除以强制 make 命令再次构建 app。
- 现在调用 make 命令来处理 mkfile.w,我们得到了与 第 1 项 相同的结果。
- 再次使用 mkfile.r 调用 make 命令,但由于 目标 已是最新状态,因此未执行任何操作。
- 与 第 4 项 相同的结果,这次处理 mkfile.w。
- touch 命令用于更改 inc_a.h 的时间戳,模拟对文件所做的更改。
- mkfile.w 未识别 inc_a.h 模块中的更改。
- mkfile.r 按预期处理。
- 调用 touch 命令更改 app.c 的访问时间,模拟对文件所做的更改。
- mkfile.w 按预期处理。
- 调用 touch 命令更改 app.c 的访问时间,模拟对文件所做的更改。
- mkfile.r 按预期处理。
现在您明白了为什么 mkfile.w 被认为是一个糟糕或不完整的 makefile。它没有考虑到 inc_a.h 模块,因为它没有在 app 目标 的 依赖项列表 中描述。
2.2 测试 sample2
sample2 是另一个简单的 makefile 示例,但这次有不止一个 目标。同样,有 2 个 makefile:mkfile.r 和 mkfile.w 来演示编写 makefile 的正确和错误方法。
如您所见,最终的可执行文件(app 目标)由 3 个对象文件组成:main.o、mod_a.o 和 mod_b.o。每个对象文件都是一个 目标,其源文件代表其 依赖项列表。
app 目标 是主 目标,或将产生主可执行文件的 目标。请注意 app 依赖项列表。它们是其他 目标 的名称。
两个 makefile 都完整。主要区别在于 目标 在 makefile 中的放置顺序。
所以,我们有
mkfile.r
app: main.o mod_a.o mod_b.o
cc -o app main.o mod_a.o mod_b.o
main.o: main.c inc_a.h inc_b.h
cc -c main.c
mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c
mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c
和
mkfile.w
main.o: main.c inc_a.h inc_b.h
cc -c main.c
mod_a.o: mod_a.c inc_a.h
cc -c mod_a.c
mod_b.o: mod_b.c inc_b.h
cc -c mod_b.c
app: main.o mod_a.o mod_b.o
cc -o app main.o mod_a.o mod_b.o
让我们尝试以下命令序列(
- 调用 make 命令来处理 mkfile.w,您可以看到只执行了第一个规则。
- 为强制 make 命令执行完整构建,删除了所有先前构建产生的对象文件。
- 调用 make 命令处理 mkfile.r,所有模块都正确创建。
- app 被执行。
- 所有对象和可执行文件都已删除,以强制 make 命令执行完整构建。
- 再次调用 make 命令来处理 mkfile.w。但这次 app 目标 作为参数传递,所有模块都正确创建。
那么,mkfile.w 有什么问题呢?好吧,从技术上讲,当您告知 主目标 时(图 3 - 第 6 项),没有任何问题。但是,当您不告知 目标 时,make 命令会从头开始读取 makefile 以查找要处理的第一个 目标。在 mkfile.w 的情况下,该 目标 是 main.o。main.o 目标 仅仅告诉 make 从 main.c、inc_a.h 和 inc_b.h 构建 main.o - 没有其他相关的事情要做。Make 将不会读取下一个 目标。
注意:读取的 第一个目标 决定了 make 必须如何解释所有其他 目标 以及在构建过程中必须遵循的顺序。因此,第一个目标 应该是 主目标,它可能与一个或多个次要 目标 相关联以执行构建。
让我们看看 app 目标。它在两个 makefile 中放置在不同的行,但它们在两个文件中具有相同的语法。因此,图 3 中的 第 3 项 和 第 6 项 将产生相同的结果
- app 目标 告诉 make 命令它有 3 个依赖项需要首先处理:main.o、mod_a.o 和 mob_b.o,然后才构建最终的可执行文件(app)。
- 然后,make 开始查找 main.o 目标并处理它。
- 之后,它找到并处理 mod_a.o。
- 最后,处理 mod_b.o。
- 当所有这 3 个 目标 都构建完成后,app 目标规则 被处理,并创建 app 可执行文件。
3. 假目标、宏和特殊字符
有时,目标 并不意味着一个 文件,而可能代表要执行的操作。当 目标 与文件无关时,它被称为 假目标。
例如:
getobj:
mv obj/*.o . 2>/dev/null
getobj 目标 将所有扩展名为 .o 的文件从 obj 目录移动到当前目录——这没什么大不了的。但是,您可能会问自己:“如果 obj 中没有文件怎么办?”这是一个好问题。在这种情况下,mv 命令将返回一个错误,该错误将传递给 make 命令。
注意:make 命令的默认行为是当在执行 规则 中的命令时检测到错误时中止处理。
当然,会遇到 obj 目录为空的情况。您如何避免 make 命令在发生错误时中止呢?
您可以在 mv 命令前面使用特殊字符 -(减号)。因此
getobj:
-mv obj/*.o . 2>/dev/null
- 告诉 make 忽略错误。还有另一个特殊字符:@ - 告诉 make 在执行命令之前不要将其打印到标准输出。您可以将两者结合起来,始终放在命令前面
getobj:
-@mv obj/*.o . 2>/dev/null
有一个特殊的 假目标 叫做 all,您可以在其中将几个 主目标 和 假目标 分组。all 假目标 通常用于在读取 makefile 时引导 make 命令。
例如:
all: getobj app install putobj
make 命令将按顺序执行 目标:getobj, app, install 和 putobj。
另一个有趣的特性是,make 命令支持 makefile 中的 宏 概念。我们可以通过编写以下内容来定义一个 宏
MACRONAME=value
并通过编写 $(MACRONAME) 或 ${MACRONAME} 来访问 MACRONAME 的值。
例如:
EXECPATH=./bin
INCPATH=./include
OBJPATH=./obj
CC=cc
CFLAGS=-g -Wall -I$(INCPATH)
执行时,make 将 $(MACRONAME) 替换为适当的定义。现在我们知道什么是 假目标 和 宏,我们可以进入下一个示例。
3.1 测试 sample3
sample3 有一个稍微复杂一点的 makefile,它使用了 宏、假目标 和 特殊字符。此外,当您列出 sample3 目录时,您可以看到 3 个子目录:
- include - 所有 .h 文件所在的位置。
- obj - 所有对象文件在构建后移动到此目录,并在开始新构建之前从该目录移出。
- bin - 最终可执行文件复制到此目录。
.c 源文件与 makefile 一起保存在 sample3 根目录中。当 makefile 文件名为 makefile 时,不需要在 make 命令中使用 -f 选项。
文件分离到目录中,使此示例更具真实性。makefile 列表如下
当然,makefile 中不存在行号。我在这里使用它们只是为了更容易阅读源代码。
所以,我们有
- 第 7 - 13 行:定义一些 宏:
- INSTPATH、INCPATH 和 OBJPATH 指的是子目录。
- CC 和 CFLAGS 分别是编译器和最常见的编译器选项。注意,如果您愿意,可以将 CC 更改为指向 gcc 编译器。
- COND1 和 COND2 是在引用宏时要执行的命令。
- 第 17 行:all 假目标 作为 makefile 中第一个 目标。all 目标 定义了目标将从左到右执行的顺序。
- getobj 是第一个,后面是 app(main.o、mod_a.o 和 mod_b.o 是 app 的依赖项,必要时会调用),接下来 make 调用 install 目标,最后执行 putobj。
- 第 19-29 行:列出负责构建应用程序本身的 目标。注意 CC 和 CFLAGS 宏的使用。请记住宏会被值替换。因此 $(CC) 被读取为 cc,CFLAGS 被读取为 -g -Wall -I./include。然后,第 20 行 被解释为
- -g -Wall -I./include -o app main.o mod_a.o mod_b.o
- 第 31-34 行:列出 getobj 和 putobj 目标。这些都是 假目标,有助于或组织构建过程。
- getobj 在 all 目标中被引用,以便首先执行。它负责在构建开始之前从 obj 目录 ($(OBJPATH)) 获取对象文件。因此,make 命令可以比较对象的时间戳与源代码文件的时间戳,并根据源文件的更改重建或不重建对象文件。
- putobj 执行相反的操作。成功构建后,它将所有对象文件移回 obj 目录。
- 第 38 行:install 目标是另一个 假目标,它展示了 if shell 语句的使用。shell 编程不在本文的范围之内。因此,如果您想了解更多信息,请在 Google 上搜索。install 目标的作用将在本文后面解释。
- 第 47 行:cleanall 目标删除所有文件,以强制 make 命令重新构建所有文件。它在构建过程中不被调用。您可以通过将其作为参数传递给 make 命令来调用它:
make cleanall
您还应该注意到在 getobj、putobj、install 和 cleanall 中的命令前面使用了特殊字符(- 和 @)。如前所述,- 告诉 make 即使发生错误也继续处理,而 @ 告诉 make 在执行命令之前不要打印 命令。
注意:在 install 目标 中,每行都以“\”结尾,并且“\”字符必须是行中的最后一个字符(其后不能有空格),否则 make 可能会发出以下错误:
line xxx: syntax error: unexpected end of file
其中 xxx 是从块开始计算的行号。
事实上,每次您想使用 \ 对命令进行分组时,它都必须是行中的最后一个字符。
让我们尝试以下命令序列(图 5):
- 由于 makefile 名称是 makefile,因此在不带任何参数的情况下调用 make 命令。
- 如果 obj 目录中没有文件,getobj 将发出错误。但是,mv 命令前面的 - (减号) 字符(第 32 行)会阻止 make 中止处理。
- 只有当 if 条件为 TRUE 时(第 39 行)才会打印该消息。首先要注意的是 if 语句前面的 @ 字符。这使得整个块都不会被打印。该条件比较两个字符串,这两个字符串是 COND1 和 COND2 宏(第 12-13 行)定义的两个命令的结果。该条件使用 shell 命令的组合来验证 app 和 ./bin/myapp 的时间戳是否不同。如果为 true,则将 app 复制到 ./bin,命名为 myapp,并更改其文件权限,只允许文件所有者对其拥有访问权限。如果没有施加任何条件,则每次调用 make 时都会执行 install 目标。
- 再次调用 make 命令,但只执行 getobj 和 putobj。没有发生构建,因此也没有 install。
- touch 命令更改了 inc_a.h 的时间戳。
- 调用 make 命令,只重建了将 inc_a.h 作为依赖项的目标。
- 只需列出 ./bin 的内容。注意 myapp 的权限。
- 如何使用 cleanall 目标 的示例。
4. 后缀规则:简化 Makefiles 语法
当您的项目变得复杂,包含许多源文件时,为每个源文件创建目标是不切实际的。例如,如果您的项目有 20 个 .c 文件来构建一个可执行文件,并且每个源文件在构建时都使用相同的编译器标志集,那么应该有一种方法可以指示 make 命令对所有源文件执行相同的 命令。
我所说的这种方法称为 后缀规则 或基于文件扩展名的规则。例如,以下后缀规则
.c.o:
cc -c $<
告诉 make 命令:给定一个扩展名为 .o 的 目标 文件,应该有一个扩展名为 .c 的依赖文件(同名——只更改扩展名),该文件可以构建。因此,文件 main.c 将生成文件 main.o。注意 .c.o 不是一个 目标,而是两个扩展名(.c 和 .o)。
后缀规则的语法是
特殊字符 $< 将在本文后面解释。
4.1 测试 sample4 - mkfile1
让我们尝试 sample4。有一些 makefile 可以测试。第一个,mkfile1
.c.o:
cc -c $<
您会看到它只包含一个 后缀规则 定义,没有其他内容。
让我们尝试以下命令序列(图 7):
- 调用 make 命令来处理 mkfile1 makefile。什么也没发生,因为没有定义 目标。
- 再次调用 make 命令,但这次传递 main.o 目标。这次 make 命令根据 .c.o 后缀规则编译 main.c。
这里有一个需要理解的地方。mkfile1 只定义了一个 后缀规则,没有 目标。那么,为什么 main.c 会被编译呢?
make 命令处理 main.o,就好像在 mkfile1 中定义了以下目标一样
main.o: main.c
cc -c main.c
因此,后缀规则 .c.o 告诉 make 命令:“对于每个 xxxxx.o 目标,必须有一个 xxxxx.c 依赖项来构建。”
如果命令是:
make -f mkfile1 mod_x.o
make 将返回错误,因为目录中没有 mod_x.c。
- 调用 make 命令,传递两个 目标。
- 调用 make 命令,传递三个 目标。由于这些 目标 已经构建,因此它不再重新构建它们。
4.2 更多特殊字符
后缀规则 中定义的 $< 是什么意思?它表示 当前依赖项的名称。在 .c.o 后缀规则 的情况下,当规则执行时,$< 被 xxxxx.c 文件替换。还有其他
$? | 比当前目标更新的依赖项列表。 |
$@ | 当前目标的名称。 |
$< | 当前依赖项的名称。 |
$* | 不带扩展名的当前依赖项名称。 |
接下来的这些示例将展示这些字符的使用。
4.3 测试 sample4 - mkfile2
mkfile2 展示了使用 后缀规则 的另一种方式。现在它只将 file.txt 重命名为 file.log
.SUFFIXES: .txt .log
.txt.log:
@echo "Converting " $< " to " $*.log
mv $< $*.log
关键字 .SUFFIXES: 告诉 make 命令在 makefile 中将使用哪些文件扩展名。在 mkfile2 的情况下,它们是 .txt 和 .log。某些扩展名,如 .c 和 .o,是默认的,无需使用 .SUFFIXES: 关键字进行定义。
让我们尝试以下命令序列(图 8):
- 首先,创建 file.txt。
- 调用 make 命令,传递 file.log 目标,并执行规则。
后缀规则 .txt.log 告诉 make 命令:“对于每个 xxxxx.log 目标,必须有一个 xxxxx.txt 依赖项才能构建(在这种情况下,重命名)。” 它的工作方式就好像 file.log 目标在 mkfile2 中定义了一样
file.log: file.txt mv file.txt file.log
- 显示 file.txt 已重命名为 file.log。
4.4 测试 sample4 - mkfile3 和 mkfile4
到目前为止,我们已经看到了如何定义 后缀规则,但我们还没有在实际情况中使用它们。所以,让我们通过使用 sample4 目录中的 C 源代码进入更真实的场景。
首先让我们尝试 mkfile3
.c.o:
@echo "Compiling" $< "..."
cc -c $<
app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."
cc -o app main.o mod_a.o mod_b.o
您会看到开头有一个后缀规则和 app 目标。
- 调用 make 命令来处理 mkfile3 makefile。make 命令读取 app 目标 并处理依赖项:main.o、mod_a.o 和 mod_b.o - 遵循 后缀规则 .c.o make 命令知道:“对于每个 xxxxx.o,都有一个依赖项 xxxxx.c 需要构建。”
- 再次调用 make 命令,但由于 app 目标 是最新状态,因此未进行任何处理。
- main.c 的访问时间已更新,以强制 make 命令重新编译它。
- make 命令只重新编译了 main.c,如预期。
- 调用 make 命令,但由于 app 目标 是最新状态,因此未进行任何处理。
- inc_a.h(由 main.c 和 mod_a.c 包含)已更新,以强制 make 命令重新编译这些模块。
- 调用 make 命令,但什么也没发生(?)
第 7 项 出了什么问题?好吧,没有任何东西告诉 make 命令 inc_a.h 是 main.c 或 mod_a.c 的依赖项。
一个解决方案是为每个 对象目标 编写依赖项
.c.o: @echo "Compiling" $< "..." cc -c $< app: main.o mod_a.o mod_b.o @echo "Building target" $@ "..." cc -o app main.o mod_a.o mod_b.o main.o: inc_a.h inc_b.h mod_a.o: inc_a.h mod_b.o: inc_b.h
您可以编辑并添加最后三行,然后再次尝试 图 9 的命令序列。不要忘记在再次测试之前删除对象
rm -f *.o app
好吧,为每个模块添加依赖项,指出每个模块包含的确切头文件是好的,但不够实用。想象一下您的项目有 50 个 .c 和 30 个 .h。那将是大量的输入!
一个更实用的解决方案可以在 mkfile4 中看到:
OBJS=main.o mod_a.o mod_b.o
.c.o:
@echo "Compiling" $< "..."
cc -c $<
app: main.o mod_a.o mod_b.o
@echo "Building target" $@ "..."
cc -o app main.o mod_a.o mod_b.o
$(OBJS): inc_a.h inc_b.h
让我们尝试以下命令序列,看看会发生什么(图 10)
- 调用 make 命令来处理 mkfile4 makefile。
- > mod_a.c 的时间戳已更新,以强制 make 命令重新编译它。
- make 命令只重新编译了 mod_a.c,如预期。
- 更新 inc_a.h(被 main.c 和 mod_a.c 包含)以强制 make 命令重新编译这些模块。
- make 命令重新编译了所有模块(?)
为什么 mod_b.c 在 第 5 项 被重新编译?
mkfile4 将 inc_a.h 定义为 mod_b.c 的依赖项,但它不是。我的意思是,mod_b.c 没有包含 inc_a.h。实际上,makefile 告诉 make 命令 inc_a.h 和 inc_b.h 是所有模块的依赖项。我以这种方式编写 mkfile4 是因为它更实用,这并不意味着存在任何错误。如果您愿意,可以按模块分离头文件。
提示:在处理大型项目时,我通常只将主头文件作为依赖项。这意味着,这些头文件被所有(或大多数)模块包含。
5. 一个调用其他 Makefiles 的 Makefile
当您在一个包含许多不同部分(如库、DLL、可执行文件)的大型项目中工作时,最好将它们拆分为目录结构,将每个模块的源文件保存在自己的目录中。这样,每个源目录都可以有自己的 makefile,并可以由一个 主 makefile 调用。
主 makefile 保存在根目录中,它会切换到每个子目录以调用模块的 makefile。这听起来很简单,而且确实如此。
但是,当您强制 make 命令切换到其他目录时,您应该知道一个技巧。例如,让我们测试一下简单的 makefile
target1:
@pwd
cd dir_test
@pwd
通过调用 make 命令,结果是
pwd 命令打印当前目录。在这种情况下是 /root 目录。请注意,第二个 pwd 命令打印相同的目录,即使在执行 cd dir_test 之后。这意味着什么?
您应该知道,大多数 shell 命令(如 cp、mv 等)会强制 make 命令
- 打开一个新的 shell 实例;
- 执行命令;
- 关闭 shell 实例;
事实上,make 创建了三个不同的 shell 实例来处理每个命令。
cd dir_test 仅在 make 创建的第二个 shell 实例中成功执行。
解决方案如下
target1:
(pwd;cd dir_test;pwd)
见结果
括号确保所有命令都由单个 shell 处理 - make 命令只启动一个 shell 来执行所有三个命令。
所以,您可以想象当您不使用括号并且您的 makefile 是这样的时候会发生什么
target1:
cd dir_test
make
见结果
你看到当你不用括号尝试时会发生什么吗?它正在递归调用同一个 makefile。例如,make[37] 意味着第 37 个 make 命令实例。
5.1 测试 sample5
sample5 演示了如何通过 主 makefile 调用其他 makefile。当您列出 sample5 目录时,您会发现
- tstlib 目录:包含一个简单库 (tlib.a) 的源代码。
- application 目录:包含链接到 tlib.a 的应用程序源代码。
- makefile:主 makefile。
- runmk:一个调用 主 makefile 的 shell。您并不真正需要这个 shell,但是当 make 命令处理一个切换到其他目录的 makefile 时,make 习惯于显示一个烦人的消息,告知它正在进入或离开一个目录。您应该使用 --no-print-directory 来避免这些消息。
主 makefile 列表
COND1=`stat app 2>/dev/null | grep Modify`
COND2=`stat ./application/app 2>/dev/null | grep Modify`
all: buildall getexec
buildall:
@echo "****** Invoking tstlib/makefile"
(cd tstlib; $(MAKE))
@echo "****** Invoking application/makefile"
(cd application; $(MAKE))
getexec:
@if [ "$(COND1)" != "$(COND2)" ];\
then\
echo "Getting new app!";\
cp -p ./application/app . 2>/dev/null;\
chmod 700 app;\
else\
echo "Nothing done!";\
fi
cleanall:
-rm -f app
@echo "****** Invoking tstlib/makefile"
@(cd tstlib; $(MAKE) cleanall)
@echo "****** Invoking appl/makefile"
@(cd application; $(MAKE) cleanall)
这没什么大不了的。4 个假目标。all 目标 开始调用 buildall 目标,您会看到 make 命令被命令进入并尝试构建两个不同的项目。注意 $(MAKE) 宏被 make 单词替换。$(MAKE) 宏是默认的,不需要定义。
cleanall 目标 被间接用于删除所有对象和可执行文件。注意 @ 字符在开放括号前,以强制 make 不打印命令。
让我们尝试以下命令序列(图 14):
- 通过 runmk shell 调用 make 命令来处理 主 makefile。
- all 目标 调用 buildall 目标,因为它是一个假目标,所以它总是会被执行。首先,调用 tstlib 目录中的 makefile,并构建 tlib.a。请参阅 makefile 列表
TLIB=tlib.a OBJS=tstlib_a.o tstlib_b.o CC=cc INCPATH=. CFLAGS=-Wall -I$(INCPATH) .c.o: $(CC) $(CFLAGS) -c $< $(TLIB): $(OBJS) ar cr $(TLIB) $(OBJS) $(OBJS): $(INCPATH)/tstlib.h cleanall: -rm -f *.o *.a
- 接下来,调用 application 目录中的 makefile,并构建 app。请参阅 makefile 列表
OBJS=main.o mod_a.o mod_b.o CC=cc INCLIB=../tstlib LIBS=$(INCLIB)/tlib.a CFLAGS=-Wall -I. -I$(INCLIB) .c.o: $(CC) $(CFLAGS) -c $< app: $(OBJS) $(LIBS) $(CC) $(CFLAGS) -o app $(OBJS) $(LIBS) $(OBJS): inc_a.h inc_b.h $(INCLIB)/tstlib.h cleanall: -rm -f *.o app
请注意,tlib.a 是 app 目标 的依赖项。因此,每当 tlib.a 重建时,app 必须再次链接到它。
- 在 主 makefile 中调用了 getexec 目标。由于 app 可执行文件不存在于 sample5 目录中,因此它从 application 目录复制。
- all 目标 调用 buildall 目标,因为它是一个假目标,所以它总是会被执行。首先,调用 tstlib 目录中的 makefile,并构建 tlib.a。请参阅 makefile 列表
- touch 命令用于更改 tstlib/tstlib_b.c 的时间戳,模拟对文件所做的更改。
- 通过 runmk shell 调用 make 命令来处理 主 makefile。
- tlib.a 被重建。
- app 再次链接到 tlib.a,因为它已更改。
- 在 主 makefile 中调用了 getexec 目标。由于 application/app 与 app 不同,它在 sample5 目录中更新。
- touch 命令用于更改 application/inc_a.h 的时间戳,模拟对文件所做的更改。
- 通过 runmk shell 调用 make 命令来处理 主 makefile。
- tlib.a 未被处理,因为它没有更改。
- app 重新构建(所有模块),因为 inc_a.h 是所有 .c 源文件的依赖项。
- 在 主 makefile 中调用了 getexec 目标。由于 application/app 与 app 不同,它在 sample5 目录中更新。
- 在 主 makefile 中执行 cleanall 目标。
6. 结论
正如您通过本文所看到的,make 命令是一个功能强大的工具,可以极大地帮助您的项目。正如我所说,本文无意成为一个教程,而只是对如何编写 makefile 的基本方面的参考。互联网上有很多关于 make 命令和 makefile 的信息。此外,还有一些书籍涵盖了本文中未提及的其他功能。
希望这有所帮助。
历史
First version.