Makefile

来自osdev
Zhang3讨论 | 贡献2022年4月21日 (四) 05:30的版本 (创建页面,内容为“{{Rating|1}} Makefile是控制“make”命令的文件。 Make在几乎所有平台上都可用,并用于控制项目的构建过程。 为项目编写Makefile后,make可以轻松有效地构建项目并创建所需的输出文件。 Make从Makefile中读取依赖项信息,找出哪些输出文件需要(重新)构建(因为它们缺少或比相应的输入文件旧),执行必要的命令来实际“重新”构建。 与总是重新构建…”)
(差异) ←上一版本 | 最后版本 (差异) | 下一版本→ (差异)
跳到导航 跳到搜索
难度等级
Difficulty 1.png
初学者

Makefile是控制“make”命令的文件。 Make在几乎所有平台上都可用,并用于控制项目的构建过程。 为项目编写Makefile后,make可以轻松有效地构建项目并创建所需的输出文件。

Make从Makefile中读取依赖项信息,找出哪些输出文件需要(重新)构建(因为它们缺少或比相应的输入文件旧),执行必要的命令来实际“重新”构建。 与总是重新构建整个项目的“构建批处理文件”相比,这是非常有利的。

在这样做时,make不限于任何特定的语言集。 任何获取输入文件并生成输出文件的工具都可以通过make进行控制。

一个Makefile可以提供几个不同的“目标(targets)”,这些“目标”可以由‘make <target>’调用。 一个经常使用的目标是 'make clean',它删除任何临时文件,或'make install',它安装项目在其目标位置-但你也可以给出一个特定的输出文件作为目标,并让make只处理那个文件。 在没有目标参数的情况下调用make会构建默认目标(通常是构建整个项目)。

但是‘make’需要一个编写良好的Makefile才能高效可靠地完成这些工作。

Makefile教程

“make”手册将告诉你可以用一个Makefile做的数百件事,但它没有给你一个“好的”Makefile的例子。 以下示例大多是根据我当时使用的真实PDCLibMakefile创建的, 并显示了其中使用的一些 “技巧”,对于初学者来说可能并不那么明显。

Makefile只创建一个项目范围的链接器库,但对于多个二进制文件/库来说,扩展它应该很容易。

(注意:Make 相关有几种不同的风格,POSIX为它们定义了一个共同点。 本教程专门针对GNU make。 有关更多信息,请参见讨论页

基础知识

最好的做法是命名你的Makefile为 Makefile (不带扩展名), 因为在没有任何参数的情况下执行make时,默认情况下它会在当前目录中查找该文件。 它还使文件显示在目录列表顶部的显著位置,至少在Unix机器上是这样。

一般来说,Makefile由「definitions 定义」和「rules 规则」组成。

definition声明一个变量,并为其赋值。 其一般语法为VARIABLE := Value

注意: 通常,你会看到使用“=”而不是“:=””的定义。 这样的定义将导致赋值“在每次使用变量时”被求值,而“:=”在make启动时只求值一次,这通常是你想要的。 尽管-“:=” 是make语法的GNU扩展名,但不要更改其他人的makefile。

Rule定义了一个target,0..n个dependencies依赖项和0..n个commands命令。 一般的想法是make检查目标(文件)是否在那里; 如果不是,或者任何依赖项都比目标更新,则执行命令。 一般语法是:

target: dependency
	command

dependencycommand都是可选的。 可能有多个command行,在这种情况下,它们是按顺序执行的。

请注意,command“必须使用缩进制表符”。 如果你的编辑器环境设置为用空格替换制表符,则必须在编辑生成文件时撤消该设置。

对于初学者来说,Makefiles如此难以阅读的原因是它不是语法上程序性的 (即自上而下执行), 而是功能性的:'make'读取“整个”Makefile,构建一个依赖关系树,然后根据需要通过从一个规则跳到另一个规则来解析依赖关系,直到你在命令行上给出的目标成功解析为止。

但是,我们不要进入 “make” 的内部细节。 这是一个教程,不是手册页,因此它将向你展示如何构建现实生活中的Makefile,以及每一行背后的想法。

自动化测试

由于本教程中介绍的Makefile以特殊的方式处理自动化测试,因此将在前面解释这种方法,因此本教程的相关部分对你来说很有意义。

如上所述,本教程主要源于PDCLib项目的工作,该项目构建了一个链接器库。 在该项目中,每个源文件严格只有一个库函数。 在每个这样的源文件中,都有一个用于条件编译的测试驱动程序,如下所示:

#ifdef TEST
int main()
{
    /* Test function code here */
    return NUMBER_OF_TEST_ERRORS;
}
#endif

因此,当使用gcc-c编译该源代码时,它会生成一个包含库函数代码的目标文件; 当用gcc -DTEST编译时,它为该函数提供了可执行的测试驱动程序。 返回错误数量允许对遇到的错误进行总计 (请参见下文)。

文件列表

首先,各种“文件列表”被组合在一起,这些列表稍后在Makefile中需要。

非源文件

# 这是属于发行版的所有非源文件的列表。
AUXFILES := Makefile Readme.txt

再往下走,我们将有一个目标dist,它将所有需要的文件打包到一个tarball中以供分发。 无论如何,都会创建源文件和头文件的列表。 但通常会有一些辅助文件,它们不会在Makefile中的任何其他地方被引用,但应该仍然会在tarball中结束。 这些都列在这里。

项目目录

PROJDIRS := functions includes internals

这些是包含实际源代码的子目录。 (或者更确切地说,自动搜索源文件,见下文。)这些可能是子项目,或者其他什么。 我们可以简单地搜索从当前工作目录开始的源文件,但是如果你想在你的项目中有临时子目录 (用于测试,保留参考来源等),那就行不通了。

请注意,这不应该是一种递归构建的方法;这些子目录中没有Makefile。 如果使用一个生成文件,依赖项就很难正确处理。 本篇文章的底部是一篇关于 “递归制造被认为是有害的” 主题的非常好的论文的链接; 一个Makefile不仅更容易维护(一旦你学会了几个技巧),它还可以更高效!

源文件和头文件

SRCFILES := $(shell find $(PROJDIRS) -type f -name "\*.c")
HDRFILES := $(shell find $(PROJDIRS) -type f -name "\*.h")

这两行是做什么的应该显而易见。现在,我们的项目目录中有所有源文件和头文件的列表。

目标文件和测试驱动程序可执行文件

OBJFILES := $(patsubst %.c,%.o,$(SRCFILES))
TSTFILES := $(patsubst %.c,%_t,$(SRCFILES))

OBJFILES应该被清除--源文件的列表,用‘’*.c‘’替换为‘’*.o‘’。 TSTFILES 对文件名后缀 *_t 执行相同的操作,我们将将其用于测试驱动程序可执行文件。

注意:本教程最初使用*.t,而不是这里的*_t; 然而,这使得我们无法将测试驱动程序可执行文件的依赖项与库对象文件的依赖项分开处理,这是必要的。 见下一节。

依赖项,第一部分

许多人每次在代码中的某个地方添加/更改#include时都会编辑他们的Makefile。(或者忘记这样做,导致一些人摸不着头脑。)

但是大多数编译器-包括 GCC -可以自动为你完成此操作! 尽管这种方法看起来有点落后。 对于每个源文件,GCC将创建一个依赖关系文件(它通常以*.d结尾),其中包含一个Makefile依赖关系规则,该规则列出了源文件包含的内容。(还有更多,但见下文。)

我们需要两组独立的依赖文件; 一个用于库对象,一个用于测试驱动程序可执行文件(这些文件通常具有额外的include,因此也具有依赖性,而OBJFILES不需要这些文件)。

DEPFILES    := $(patsubst %.c,%.d,$(SRCFILES))
TSTDEPFILES := $(patsubst %,%.d,$(TSTFILES))

分发文件

最后一个列表是包含所有源文件,头文件和辅助文件的列表,这些文件应最终包含在分发tarball中。

ALLFILES := $(SRCFILES) $(HDRFILES) $(AUXFILES)

.PHONY

下一个会让你大吃一惊。 当你为make clean编写规则时,如果你的工作目录中恰好有一个名为 clean的文件,你可能会惊讶地发现make什么也不做,因为规则clean的“目标”已经存在。 为避免这种情况,将此类规则声明为 phony,即禁用对该名称的文件的检查。 每次都将执行以下操作:

.PHONY: all clean dist check testdrivers todolist

CFLAGS

如果你认为-Wall真的告诉了你一切,那么你现在会有一个意料之外的惊喜。 如果你连-Wall都不用,你就丢脸了。;)

WARNINGS := -Wall -Wextra -pedantic -Wshadow -Wpointer-arith -Wcast-align \
            -Wwrite-strings -Wmissing-prototypes -Wmissing-declarations \
            -Wredundant-decls -Wnested-externs -Winline -Wno-long-long \
            -Wconversion -Wstrict-prototypes
CFLAGS := -g -std=gnu99 $(WARNINGS)

建议在项目中一次添加一个新的警告选项,而不是一次添加所有警告选项,以避免被警告淹没。;) 这些标志仅仅是对C语言工作的建议。 如果你使用C++,你需要不同的。 查看GCC手册;每次主要的编译器更新都会更改可用警告选项的列表。

Rule(规则)

现在rule来了,以其典型的backward方式(首先是表明顶层rule)。 最高rule是默认rule (也就是,如果调用 'make' 而没有显式target)。 第一条rule叫做“all”,这是常见的做法。

顶级目标

all: pdclib.a

pdclib.a: $(OBJFILES)
	@ar r pdclib.a $?

clean:
	-@$(RM) $(wildcard $(OBJFILES) $(DEPFILES) $(TSTFILES) pdclib.a pdclib.tgz)

dist:
	@tar czf pdclib.tgz $(ALLFILES)

行开头的 “@” 告诉 “make” 保持安静,即在执行命令之前不要在控制台上回显已执行的命令。 Unix的信条是“没有消息就是好消息”。 你也不会得到带有cptar的已处理文件的列表,所以我完全不明白为什么开发人员选择让他们的Makefile大量生成无用的垃圾列表。 关闭 “make” 消息的一个非常实际的优点是,你实际上可以 “看到” 那些编译器警告,而不是让它们被噪音淹没。

行开头的“-”告诉“make”即使遇到错误也要继续(默认行为是终止整个生成)。

规则中的$(RM)是用于删除文件的独立于平台的命令。

pdclib.a规则中的 $? 是一个内部变量,“make” 扩展以列出规则 “比目标更新的”所有依赖项。

自动测试,第二部分

check: testdrivers
	-@rc=0; count=0; \
        for file in $(TSTFILES); do \
            echo " TST     $$file"; ./$$file; \
            rc=`expr $$rc + $$?`; count=`expr $$count + 1`; \
        done; \
	echo; echo "Tests executed: $$count  Tests failed: $$rc"

testdrivers: $(TSTFILES)

尽管有点简陋,但对我来说效果很好。 前导的“-”表示遇到错误时“make”不会中止,而是继续循环。

如果你从某个测试驱动程序那里得到了SEGFAULT或类似的东西,则其中的echo " TST $$file"非常有用。 (在执行驱动程序时,如果不响应驱动程序,你将不知所措。)

依赖项,第二部分

-include $(DEPFILES) $(TSTDEPFILES)

在下面,你将看到依赖文件是如何 “生成” 的。 在这里,我们“包括”了所有这些,也就是说,将它们中列出的依赖项作为Makefile的一部分。 当我们第一次运行Makefile时,它们可能根本不存在--前导的“-”再次抑制错误,这并不重要。

提取TODO语句

todolist:
	-@for file in $(ALLFILES:Makefile=); do fgrep -H -e TODO -e FIXME $$file; done; true

获取项目中的所有文件 (Makefile本身除外),这将 “grep” 文件中的所有 “todo” 和 “fixme” 注释,并将其显示在终端中。 在发布之前还能记住仍然缺失的内容这很好。 要添加另一个关键字,只需插入另一个-e keyword

Note: $(ALLFILES:Makefile =) 是 $(ALLFILES) 中所有内容的列表,但字符串 “Makefile” 除外,该字符串 “Makefile” 被替换为任何内容 (即从列表中删除)。 这避免了自引用匹配,即grep命令中的字符串“TODO”将找到Makefile自己。 (当然,它也导致了不能在Makefile中找到TODO,但总有一个缺点。;-) )

依赖项,第三部分

现在是我之前提到的依赖魔法。 请注意,这需要GCC 3.3或更高版本。

%.o: %.c Makefile
	@$(CC) $(CFLAGS) -MMD -MP -c $< -o $@

这难道不美吗?;-)

“-MMD”标志生成依赖项文件(%.d),该文件将保存(在Makefile语法中)规则,使“生成的”文件(%.o)依赖于源文件“”及其包含的任何非系统头“”。 这意味着每当相关源文件被修改时,对象文件都会被自动重新创建。 如果你还想依赖于系统标头 (即,检查它们在每次编译上的更新),请改用 “-MD”。 -MP选项添加了空的伪规则,可以避免从文件系统中删除头文件时出现错误。

编译对象文件实际上看起来像一个副作用。;-)

请注意,规则的依赖项列表包括Makefile本身。 如果你更改了生成文件中的CFLAGS,你希望应用它们,不是吗? 在命令中使用 $< macro ("first dependency") 可以确保我们不尝试将Makefile做为C源代码编译。

当然,我们还需要生成测试驱动程序可执行文件(及其依赖文件)的规则:

%_t: %.c Makefile pdclib.a
	@$(CC) $(CFLAGS) -MMD -MP -DTEST $< pdclib.a -o $@

在这里,你可以看到为什么测试驱动程序可执行文件会得到一个*_t后缀,而不是一个*.t扩展名: -MMD选项使用compiled文件的基本名称(即不带扩展名的文件名)作为依赖项文件的基础。 如果我们将源编译为 abc.oabc.t,则依赖项文件都将被命名为 abc.d,相互覆盖。

其他编译器对此的支持有所不同,所以在使用“GCC”以外的东西时,请查看它们的详细信息。

备份

备份你的文件是一项很应该自动化的任务。 然而,这通常是通过版本控制系统来实现的

下面是 “备份” 目标的一个示例,该目标创建了Makefile所在目录的日期为7-Zip的存档。

THISDIR := $(shell basename `pwd`)
TODAY := $(shell date +%Y-%m-%d)
BACKUPDIR := projectBackups/$(TODAY)

backup: clean
	@tar cf - ../$(THISDIR) | 7za a -si ../$(BACKUPDIR)/$(THISDIR).$(TODAY)_`date +%H%M`.tar.7z

总结

编写良好的Makefile可以使维护代码库变得更加容易,因为它可以将复杂的命令序列包装成简单的 “make” 调用。 特别是对于大型项目,与愚蠢的“build.sh”脚本相比,它还缩短了编译时间。 而且,一旦编写完成,几乎不需要对编写良好的Makefile进行修改。

高级技术

我们上面做的一些事情已经“非常”先进了,没有错。 但我们需要这些功能来进行基本而方便的设置。 在下面,你会发现一些甚至更棘手的东西,这些东西可能并不适合所有人,但是如果你需要的话,它将非常有帮助。

条件评估

有时,对某些环境变量的存在或内容做出反应是有用的。 例如,你可能必须依赖于框架路径中传递的某个框架的路径。 也许编译器给出的错误消息在变量未设置的情况下是没有帮助的,或者它需要很长时间,直到 “make” 到达它实际检测到错误的程度。 你希望尽早失败,并显示一条有意义的错误消息。

幸运的是,‘make’允许条件求值和手动错误报告,非常类似于C预处理器:

ifndef FRAMEWORK_PATH
    $(error FRAMEWORK_PATH is not set. Please set to the path where you installed "Framework".)
endif

ifneq ($(FRAMEWORK_PATH),/usr/lib/framework)
    $(warning FRAMEWORK_PATH is set to $(FRAMEWORK_PATH), not /usr/lib/framework. Are you sure this is correct?)
endif

多目标

编写一个改进的Makefile的初步工作已经完成,该文件允许使用单独的设置生成多个二进制文件,同时仍然易于配置和使用。 虽然还不是“教程”格式,但注释源代码可以在 这里找到。

另见

文章

  • User:Solar/Makefile, 这是一个更复杂的示例,能够从单个Makefile构建多个可执行文件和库。

外部链接


de:Makefile