65.9K
CodeProject 正在变化。 阅读更多。
Home

Linux 中的 Makefiles: 概述

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (83投票s)

2008 年 12 月 4 日

CPOL

23分钟阅读

viewsIcon

454947

downloadIcon

5452

解释 make 命令的用法和 makefile 的语法。

引言

包含几个模块的小型 C/C++ 应用程序易于管理。开发人员可以通过直接调用编译器,并将源文件作为参数传递来轻松重新编译它们。这是一种简单的方法。然而,当项目变得过于复杂,包含许多源文件时,就需要一个工具来帮助开发人员管理项目。

我所说的工具是 make 命令。make 命令不仅用于帮助开发人员编译应用程序,还可以用于任何您想从多个输入文件生成输出文件的情况。

本文不是一个完整的教程,它侧重于 C 应用程序以及如何使用 make 命令和 makefile 来构建它们。有一个 zip 文件,其中包含许多按目录结构组织的示例。示例中最重要的文件是 makefile,而不是 C 源代码。您应该下载示例文件,并使用 unzip 命令或任何其他首选工具解压缩。因此,为了更好地理解本文,请

  1. 下载 make_samples.zip
  2. 打开终端会话。
  3. 在您的主目录中创建一个目录。
  4. make_samples.zip 移动到创建的目录中。
  5. 解压缩:unzip make_samples.zip

目录

. Make 工具:语法概述

make 命令语法是

make [选项] [目标]

您可以输入 make --help 来查看 make 命令支持的所有选项。在本文中,解释所有这些选项不在范围之内。重点是 makefile 结构及其工作原理。targetmakefile 中存在的一个标签(或定义的名称)。它将在本文后面描述。

make 需要一个 makefile 来告诉它您的应用程序应该如何构建。makefile 通常与源文件位于同一目录中,它可以有任何您想要的名称。例如,如果您的 makefile 名为 run.mk,那么要执行 make 命令,请键入

make -f run.mk

-f 选项告诉 make 命令应该处理的 makefile 名称。

还有两个特殊名称使得 -f 选项不是必需的:makefileMakefile。如果您运行 make 而不传递文件名,它将首先查找名为 makefile 的文件。如果不存在,它将查找名为 Makefile 的文件。如果您的目录中有两个文件,一个名为 makefile,另一个名为 Makefile,并且您输入

make <enter>

make 命令将处理名为 makefile 的文件。在这种情况下,如果您想让 make 命令处理 Makefile,您应该使用 -f 选项。

2. Makefiles 的基本语法

图 1:Makefile 通用语法

一个 make 文件由一组 目标依赖项规则 组成。目标 大多数时候是要创建/更新的文件。目标 依赖于 依赖项列表 中描述的一组源文件甚至其他 目标规则 是使用 依赖项列表 创建 目标 文件所需的命令。

图 1 所示,规则 部分中的每个命令都必须在以 TAB 字符开头的行上。空间问题会导致错误。此外,规则 行末尾的空格可能会导致 make 发出错误消息。

makefilemake 命令读取,该命令通过比较 依赖项列表 中源文件的时间戳来确定要构建的 目标 文件。如果自上次构建以来任何依赖项的时间戳已更改,make 命令将执行与 目标 关联的规则。

2.1 测试 sample1

现在是进行一个简单测试的时候了。源代码包含一个名为 sample1 的目录。其中有四个文件:

  • app.cinc_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.cinc_a.h目标 app依赖项inc_a.h 仅存在于 mkfile.r 中)。
  • cc -o app app.c 是用于构建 目标规则,它会考虑 依赖项列表 中文件的任何更改。

为了演示它们的工作原理,让我们尝试以下命令序列(图 2

图 2:sample1 命令序列。
  1. 调用 make 命令来处理 mkfile.r,您可以看到正在执行 规则 以创建 app 可执行文件。
  2. app 被删除以强制 make 命令再次构建 app
  3. 现在调用 make 命令来处理 mkfile.w,我们得到了与 第 1 项 相同的结果。
  4. 再次使用 mkfile.r 调用 make 命令,但由于 目标 已是最新状态,因此未执行任何操作。
  5. 第 4 项 相同的结果,这次处理 mkfile.w
  6. touch 命令用于更改 inc_a.h 的时间戳,模拟对文件所做的更改。
  7. mkfile.w 未识别 inc_a.h 模块中的更改。
  8. mkfile.r 按预期处理。
  9. 调用 touch 命令更改 app.c 的访问时间,模拟对文件所做的更改。
  10. mkfile.w 按预期处理。
  11. 调用 touch 命令更改 app.c 的访问时间,模拟对文件所做的更改。
  12. mkfile.r 按预期处理。

现在您明白了为什么 mkfile.w 被认为是一个糟糕或不完整的 makefile。它没有考虑到 inc_a.h 模块,因为它没有在 app 目标依赖项列表 中描述。

2.2 测试 sample2

sample2 是另一个简单的 makefile 示例,但这次有不止一个 目标。同样,有 2 个 makefilemkfile.rmkfile.w 来演示编写 makefile 的正确和错误方法。

如您所见,最终的可执行文件(app 目标)由 3 个对象文件组成:main.omod_a.omod_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

让我们尝试以下命令序列(

图 3
):  

图 3:sample2 命令序列。
  1. 调用 make 命令来处理 mkfile.w,您可以看到只执行了第一个规则。
  2. 为强制 make 命令执行完整构建,删除了所有先前构建产生的对象文件。
  3. 调用 make 命令处理 mkfile.r,所有模块都正确创建。
  4. app 被执行。
  5. 所有对象和可执行文件都已删除,以强制 make 命令执行完整构建。
  6. 再次调用 make 命令来处理 mkfile.w。但这次 app 目标 作为参数传递,所有模块都正确创建。

那么,mkfile.w 有什么问题呢?好吧,从技术上讲,当您告知 主目标 时(图 3 - 第 6 项),没有任何问题。但是,当您不告知 目标 时,make 命令会从头开始读取 makefile 以查找要处理的第一个 目标。在 mkfile.w 的情况下,该 目标main.omain.o 目标 仅仅告诉 makemain.c、inc_a.hinc_b.h 构建 main.o - 没有其他相关的事情要做。Make 将不会读取下一个 目标

注意:读取的 第一个目标 决定了 make 必须如何解释所有其他 目标 以及在构建过程中必须遵循的顺序。因此,第一个目标 应该是 主目标,它可能与一个或多个次要 目标 相关联以执行构建。

让我们看看 app 目标。它在两个 makefile 中放置在不同的行,但它们在两个文件中具有相同的语法。因此,图 3 中的 第 3 项第 6 项 将产生相同的结果

  • app 目标 告诉 make 命令它有 3 个依赖项需要首先处理:main.omod_a.omob_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, installputobj

另一个有趣的特性是,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 列表如下

图 4:sample3 makefile 列表。

当然,makefile 中不存在行号。我在这里使用它们只是为了更容易阅读源代码。

所以,我们有

  • 第 7 - 13 行:定义一些
    • INSTPATHINCPATHOBJPATH 指的是子目录。
    • CCCFLAGS 分别是编译器和最常见的编译器选项。注意,如果您愿意,可以将 CC 更改为指向 gcc 编译器。
    • COND1COND2 是在引用宏时要执行的命令。
  • 第 17 行all 假目标 作为 makefile 中第一个 目标all 目标 定义了目标将从左到右执行的顺序。
    • getobj 是第一个,后面是 appmain.omod_a.omod_b.oapp 的依赖项,必要时会调用),接下来 make 调用 install 目标,最后执行 putobj
  • 第 19-29 行:列出负责构建应用程序本身的 目标。注意 CCCFLAGS 宏的使用。请记住宏会被值替换。因此 $(CC) 被读取为 ccCFLAGS 被读取为 -g -Wall -I./include。然后,第 20 行 被解释为
    •  -g -Wall -I./include -o app main.o mod_a.o mod_b.o
  • 第 31-34 行:列出 getobjputobj 目标。这些都是 假目标,有助于或组织构建过程。
    • getobjall 目标中被引用,以便首先执行。它负责在构建开始之前从 obj 目录 ($(OBJPATH)) 获取对象文件。因此,make 命令可以比较对象的时间戳与源代码文件的时间戳,并根据源文件的更改重建或不重建对象文件。
    • putobj 执行相反的操作。成功构建后,它将所有对象文件移回 obj 目录。
  • 第 38 行install 目标是另一个 假目标,它展示了 if shell 语句的使用。shell 编程不在本文的范围之内。因此,如果您想了解更多信息,请在 Google 上搜索install 目标的作用将在本文后面解释。
  • 第 47 行cleanall 目标删除所有文件,以强制 make 命令重新构建所有文件。它在构建过程中不被调用。您可以通过将其作为参数传递给 make 命令来调用它:
    make cleanall

您还应该注意到在 getobjputobjinstallcleanall 中的命令前面使用了特殊字符(-@)。如前所述,- 告诉 make 即使发生错误也继续处理,而 @ 告诉 make 在执行命令之前不要打印 命令

注意:install 目标 中,每行都以“\”结尾,并且“\”字符必须是行中的最后一个字符(其后不能有空格),否则 make 可能会发出以下错误:

line xxx: syntax error: unexpected end of file

其中 xxx 是从块开始计算的行号。

事实上,每次您想使用 \ 对命令进行分组时,它都必须是行中的最后一个字符。

让我们尝试以下命令序列(图 5):

图 5:sample3 命令序列。
  1. 由于 makefile 名称是 makefile,因此在不带任何参数的情况下调用 make 命令。
    • 如果 obj 目录中没有文件,getobj 将发出错误。但是,mv 命令前面的 - (减号) 字符(第 32 行)会阻止 make 中止处理。
    • 只有当 if 条件为 TRUE 时(第 39 行)才会打印该消息。首先要注意的是 if 语句前面的 @ 字符。这使得整个块都不会被打印。该条件比较两个字符串,这两个字符串是 COND1COND2 宏(第 12-13 行)定义的两个命令的结果。该条件使用 shell 命令的组合来验证 app./bin/myapp 的时间戳是否不同。如果为 true,则将 app 复制到 ./bin,命名为 myapp,并更改其文件权限,只允许文件所有者对其拥有访问权限。如果没有施加任何条件,则每次调用 make 时都会执行 install 目标。
  2. 再次调用 make 命令,但只执行 getobjputobj。没有发生构建,因此也没有 install
  3. touch 命令更改了 inc_a.h 的时间戳。
  4. 调用 make 命令,只重建了将 inc_a.h 作为依赖项的目标。
  5. 只需列出 ./bin 的内容。注意 myapp 的权限。
  6. 如何使用 cleanall 目标 的示例。

4. 后缀规则:简化 Makefiles 语法

当您的项目变得复杂,包含许多源文件时,为每个源文件创建目标是不切实际的。例如,如果您的项目有 20 个 .c 文件来构建一个可执行文件,并且每个源文件在构建时都使用相同的编译器标志集,那么应该有一种方法可以指示 make 命令对所有源文件执行相同的 命令

我所说的这种方法称为 后缀规则 或基于文件扩展名的规则。例如,以下后缀规则

.c.o:
    cc -c $<

告诉 make 命令:给定一个扩展名为 .o目标 文件,应该有一个扩展名为 .c 的依赖文件(同名——只更改扩展名),该文件可以构建。因此,文件 main.c 将生成文件 main.o。注意 .c.o 不是一个 目标,而是两个扩展名(.c.o)。

后缀规则的语法是

 

图 6:后缀规则语法

特殊字符 $< 将在本文后面解释。

4.1 测试 sample4 - mkfile1

让我们尝试 sample4。有一些 makefile 可以测试。第一个,mkfile1

.c.o:
    cc -c $<

您会看到它只包含一个 后缀规则 定义,没有其他内容。

让我们尝试以下命令序列(图 7):

图 7:sample4 命令序列 - mkfile1。
  1. 调用 make 命令来处理 mkfile1 makefile。什么也没发生,因为没有定义 目标
  2. 再次调用 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

  1. 调用 make 命令,传递两个 目标
  2. 调用 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):

图 8:sample4 命令序列 - mkfile2。
  1. 首先,创建 file.txt。
  2. 调用 make 命令,传递 file.log 目标,并执行规则。

    后缀规则 .txt.log 告诉 make 命令:“对于每个 xxxxx.log 目标,必须有一个 xxxxx.txt 依赖项才能构建(在这种情况下,重命名)。” 它的工作方式就好像 file.log 目标在 mkfile2 中定义了一样

    file.log: file.txt
        mv file.txt file.log
  3. 显示 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 目标

让我们尝试以下命令序列(图 9):

图 9:sample4 命令序列 - mkfile3。
  1. 调用 make 命令来处理 mkfile3 makefilemake 命令读取 app 目标 并处理依赖项:main.omod_a.omod_b.o - 遵循 后缀规则 .c.o make 命令知道:“对于每个 xxxxx.o,都有一个依赖项 xxxxx.c 需要构建。”
  2. 再次调用 make 命令,但由于 app 目标 是最新状态,因此未进行任何处理。
  3. main.c 的访问时间已更新,以强制 make 命令重新编译它。
  4. make 命令只重新编译了 main.c,如预期。

  5. 调用 make 命令,但由于 app 目标 是最新状态,因此未进行任何处理。
  6. inc_a.h(由 main.cmod_a.c 包含)已更新,以强制 make 命令重新编译这些模块。
  7. 调用 make 命令,但什么也没发生(?)

第 7 项 出了什么问题?好吧,没有任何东西告诉 make 命令 inc_a.hmain.cmod_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

图 10:sample4 命令序列 - mkfile4。
  1. 调用 make 命令来处理 mkfile4 makefile。
  2. > mod_a.c 的时间戳已更新,以强制 make 命令重新编译它。
  3. make 命令只重新编译了 mod_a.c,如预期。
  4. 更新 inc_a.h(被 main.cmod_a.c 包含)以强制 make 命令重新编译这些模块。
  5. make 命令重新编译了所有模块(?)

为什么 mod_b.c第 5 项 被重新编译?

mkfile4inc_a.h 定义为 mod_b.c 的依赖项,但它不是。我的意思是,mod_b.c 没有包含 inc_a.h。实际上,makefile 告诉 make 命令 inc_a.hinc_b.h 是所有模块的依赖项。我以这种方式编写 mkfile4 是因为它更实用,这并不意味着存在任何错误。如果您愿意,可以按模块分离头文件。

提示:在处理大型项目时,我通常只将主头文件作为依赖项。这意味着,这些头文件被所有(或大多数)模块包含。

 

5. 一个调用其他 Makefiles 的 Makefile

当您在一个包含许多不同部分(如库、DLL、可执行文件)的大型项目中工作时,最好将它们拆分为目录结构,将每个模块的源文件保存在自己的目录中。这样,每个源目录都可以有自己的 makefile,并可以由一个 主 makefile 调用。

主 makefile 保存在根目录中,它会切换到每个子目录以调用模块的 makefile。这听起来很简单,而且确实如此。

但是,当您强制 make 命令切换到其他目录时,您应该知道一个技巧。例如,让我们测试一下简单的 makefile

target1:
	@pwd     
	cd dir_test     
	@pwd

通过调用 make 命令,结果是

图 11:更改当前目录 - 错误方法。

pwd 命令打印当前目录。在这种情况下是 /root 目录。请注意,第二个 pwd 命令打印相同的目录,即使在执行 cd dir_test 之后。这意味着什么?

您应该知道,大多数 shell 命令(如 cpmv 等)会强制 make 命令

  • 打开一个新的 shell 实例;
  • 执行命令;
  • 关闭 shell 实例;

事实上,make 创建了三个不同的 shell 实例来处理每个命令。

cd dir_test 仅在 make 创建的第二个 shell 实例中成功执行。

解决方案如下

target1:
	(pwd;cd dir_test;pwd)

见结果

图 12:更改当前目录 - 正确方法。

括号确保所有命令都由单个 shell 处理 - make 命令只启动一个 shell 来执行所有三个命令。

所以,您可以想象当您不使用括号并且您的 makefile 是这样的时候会发生什么

target1:
    cd dir_test
    make

见结果

 

图 13:递归调用 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):

图 14:sample5 命令序列
  1. 通过 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.aapp 目标 的依赖项。因此,每当 tlib.a 重建时,app 必须再次链接到它。

    • 主 makefile 中调用了 getexec 目标。由于 app 可执行文件不存在于 sample5 目录中,因此它从 application 目录复制。
  2. touch 命令用于更改 tstlib/tstlib_b.c 的时间戳,模拟对文件所做的更改。
  3. 通过 runmk shell 调用 make 命令来处理 主 makefile
    • tlib.a 被重建。
    • app 再次链接到 tlib.a,因为它已更改。
    • 主 makefile 中调用了 getexec 目标。由于 application/appapp 不同,它在 sample5 目录中更新。
  4. touch 命令用于更改 application/inc_a.h 的时间戳,模拟对文件所做的更改。
  5. 通过 runmk shell 调用 make 命令来处理 主 makefile
    • tlib.a 未被处理,因为它没有更改。
    • app 重新构建(所有模块),因为 inc_a.h 是所有 .c 源文件的依赖项。
    • 主 makefile 中调用了 getexec 目标。由于 application/appapp 不同,它在 sample5 目录中更新。
  6. 主 makefile 中执行 cleanall 目标。

6. 结论

正如您通过本文所看到的,make 命令是一个功能强大的工具,可以极大地帮助您的项目。正如我所说,本文无意成为一个教程,而只是对如何编写 makefile 的基本方面的参考。互联网上有很多关于 make 命令和 makefile 的信息。此外,还有一些书籍涵盖了本文中未提及的其他功能。

希望这有所帮助。

历史

First version.

© . All rights reserved.