快速查看 Make
快速了解 makefile 以及如何创建更高级的 makefile。
引言
在我其他的文章中,我使用了手动创建的 makefile。Make 是一个程序,它运行一系列步骤来产生一个输出。在大多数情况下,这些步骤涉及调用编译器来编译源代码。Make 会在采取步骤创建输出文件之前自动检查源文件是否已更新。这意味着只有程序中已更改的部分会被重新编译,而不是整个程序。
我以前一直坚持使用简单的 makefile,但是到目前为止我创建的 makefile 存在一些问题/缺少功能。
- 生成的文件与源文件混在一起
- 没有调试/发布功能
在这篇文章中,我将解释我目前拥有的 make 结构,并展示我如何扩展它以包含上述功能。
背景
有些人告诉我,他们认为手动编写 makefile 是浪费时间。有人建议使用编译器工具以及 automake 等。我使用 make 的思路如下:
优点
- 我文章中的 makefile 只是描述了构建过程。
- 我理解并能看到我的构建过程中的所有步骤。
- 我不必弄清楚编译器或 automake 进程实际上在做什么。有了 Make,我确切地知道它正在运行哪些命令。
- 阅读文章的人可以使用不同的编译器。阅读文章的人可以看到我使用的 makefile,然后根据自己的编译器配置进行操作。
- KIS - 保持简单。这是我一直喜欢遵循的黄金法则,与尝试让 IDE 生成 makefile 相比,制作 makefile 很简单。
缺点
- 我目前没有编辑器内调试功能。当报告错误时,我必须手动搜索发生错误的行号。
- 我必须花时间创建 makefile。我认为这是可以接受的。在开发过程中,我不会经常更改构建过程,而且我已经形成了一个我总是遵循(并在此处描述)的模式。
遵循本教程的先决条件
我创建了一篇 CodeProject 文章,完整展示了我是如何设置用于编译 C++ 程序的工具链的:设置开源工具链。
我将更改我在另一篇文章中使用的 makefile。你可以在这里查看它:在 C++ 中开始使用 SQLite。在阅读本文之前,你应该先阅读那篇文章以获取正确的文件。如果你想跳过,我已随本文附带了一个 zip 文件。你应该将所有文件解压到 C:\code\sqlite_hello_world。
非常简单的 Makefile 示例
一个 makefile 有一个“顶部”部分,然后是任意数量的节。顶部部分通常用于设置变量等,每个节用于提供指令,告诉 Make 如何创建文件。按照以下步骤设置项目目录并创建一个简单的小 makefile。你可以在 MINGW 中使用 Shift + Ins 粘贴每个命令,以避免打字错误。
- 运行 MINGW(点击蓝色 M)
- cd /c/code/
- mkdir make_messing
- "/c/program files/notepad++/notepad++.exe" /c/code/make_messing/makefile
- 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)
CXX=g++
fin.txt:
$(warning Making fin.txt)
echo Messing about with Make > fin.txt
这是一个简单的 Make 文件,包含“顶部”部分和一个节。第一行包含一个警告语句,它只是一种在构建过程中向屏幕打印消息的方式。第二行设置一个变量。行 fin.txt:
表示第一个节的开始。该节的布局是:
<Output file>:<prerequisites ...>
command 1
command 2
...
输出文件是输出文件的名称,先决条件是创建输出文件所需的文件。命令 1、2 等是用于创建文件的 shell 命令。每条命令都以制表符开头,并且每个节之间有一空行分隔。
在我们的例子中,我们要创建的文件是 fin.txt,我们不需要任何文件来创建它。有两个命令来创建它:第一个只是告诉我们 Make 在做什么,第二个只是一个 echo 命令,通过管道输出到名为 fin.txt 的文件。要运行它,启动 MINGW 并按照以下步骤操作:
- cd /c/code/make_messing/makefile
- make
你可以看到文件已生成。
- ls
如你所见,fin.txt 现在存在了。
- make
你现在可以看到消息 fin.txt 已最新,这表明 Make 正在执行其任务。它没有处理创建已存在文件的命令。现在,让我们稍微增强一下 makefile。
- 运行 MINGW(点击蓝色 M)
- "/c/program files/notepad++/notepad++.exe" /c/code/make_messing/makefile
- 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin.txt:
$(warning Making fin.txt)
echo Messing about with Make > fin.txt
clean:
$(warning Deleting all txt files)
rm *.txt
首先要注意的是,该文件现在有两个节。Make 如何知道要处理哪个?嗯,当它在没有参数的情况下运行时,Make 总是会运行第一个节。在本例中,fin.txt 被处理。如果你想创建不同的输出文件,你可以将其指定为第一个参数,例如 make fin.txt 或 make clean 等。
Clean 部分
另一点需要注意的是,Clean 部分不是一个真正的部分。第三行将其声明为 phony。这告诉 Make 没有名为 clean 的文件被创建,但当请求时,命令会被运行。Clean 是许多 Make 文件中都有的一个标准部分,它会删除 Make 生成的所有文件。这对于强制重新构建或在你正在修改 makefile 时很有用。你可以在备份代码之前运行 make clean 以节省空间。用这个文件做一些实验,注意 txt 文件如何出现和消失,以及如果它已经存在,Make 将如何不重新构建它。
现在,修改 makefile,使其显示如下:
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin1.txt: fin2.txt
$(warning Making fin.txt)
cat fin2.txt > fin1.txt
echo done with 2 >> fin1.txt
fin2.txt: fin3.txt
$(warning Making fin.txt)
cat fin3.txt > fin2.txt
echo done with 3 >> fin2.txt
fin3.txt: fin4.txt
$(warning Making fin.txt)
cat fin4.txt > fin3.txt
echo done with 4 >> fin3.txt
fin4.txt:
$(warning Making fin.txt)
echo We are still messing about with Make > fin4.txt
clean:
$(warning Deleting all txt files)
rm *.txt
你可以看到这个 makefile 有五个节。我们前面讨论过的 Clean 节。你可以看到 fin1.txt 依赖于 fin2,fin2 依赖于 fin3,fin3 依赖于 fin4。你可以使用 Make 来制作所有这些文件,Make 会计算出哪些文件需要重新创建。Make 还会查看文件的时间戳,所以一旦你创建了所有文件,如果你更改了 fin3.txt,你会注意到 Make 会发现并重新创建 fin2.txt 和 fin1.txt。这在编程中很有用。你让对象依赖于它们的头文件,Make 会重建需要重建的对象。这可以节省时间,因为未更改的对象不会被重建。
你还可以添加一个节来创建一个最终文件,该文件依赖于所有这些文件。
fin.txt:fin1.txt fin2.txt fin3.txt fin4.txt
$(warning putting it all together)
cat fin1.txt >> fin.txt
cat fin2.txt >> fin.txt
cat fin3.txt >> fin.txt
cat fin4.txt >> fin.txt
记住,Make 默认总是构建它看到的第一个节,所以请将其放在你的 Make 文件顶部。由于此文件依赖于所有其他文件,Make 将首先创建所有其他文件。此外,所有这些都有很多警告,告诉我们 Make 在这里做了什么,但我不会把它们放在真实的 makefile 中。
为了好玩,我们可以尝试用下面的方式来混淆 Make:
aa.txt:bb.txt
echo aa > aa.txt
bb.txt:aa.txt
echo bb > bb.txt
这无法构建,Make 会警告我们并尽力尝试。
开始使用变量
看一下下面的 Make 文件
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
fin.txt:
echo Make variable test > fin.txt
echo CXX=${CXX} >> fin.txt
echo CFG=${CFG} >> fin.txt
clean:
rm *.txt
此文件使用内置变量和外部变量。你可以使用 Make 运行它,也可以通过运行类似 make -e CFG=Test_other_var 的命令向它传递变量。如你所见,这将允许我们根据命令行参数更改我们的操作。我们还可以添加一些代码来为外部变量提供默认值,并检查它们是否是正确的值。
$(warning Starting Makefile)
CXX=g++
.PHONY: clean
ifndef CFG
CFG=DEBUG
$(warning Defaulting to $(CFG))
endif
ifneq ($(CFG),DEBUG)
ifneq ($(CFG),RELEASE)
$(error error is CFG must be DEBUG or RELEASE)
endif
endif
fin.txt:
echo Make variable test > fin.txt
echo CXX=${CXX} >> fin.txt
echo CFG=${CFG} >> fin.txt
clean:
rm *.txt
Error 命令与 Warning 命令不同,因为 Error 会退出 Make 进程。你可以在这里看到变量 CFG
默认为 DEBUG
,但用户可以输入一个值。如果该值不是 DEBUG
或 RELEASE
,则构建过程会出错。你可以使用以下命令测试此 Make 文件:
make
make -e CFG=ddd
make -e CFG=RELEASE
注意:记住在测试之间使用 make clean。
将所学付诸实践
好的,现在我们需要利用这些知识来允许我们进行调试和发布构建,并将创建的文件存储在代码目录以外的其他位置。我将使用我创建的 SQLite 示例项目来完成此操作。请按照以下说明操作:
- 运行 MINGW
- "/c/program files/notepad++/notepad++.exe" /c/code/sqlite_hello_world/makefile
- 粘贴以下代码,然后保存并关闭 Notepad++
$(warning Starting Makefile)
CXX=g++
ifndef CFG
CFG=DEBUG
$(warning Defaulting to $(CFG))
endif
ifneq ($(CFG),DEBUG)
ifneq ($(CFG),RELEASE)
$(error error is CFG must be DEBUG or RELEASE)
endif
endif
.PHONY: clean
${CFG}/main.exe: ${CFG}/dir_exists.obj ${CFG}/sqlite_demo.lib main.cpp
$(CXX) -s main.cpp -o ${CFG}/main.exe -Wl,${CFG}/sqlite_demo.lib
${CFG}/sqlite_demo.lib: ${CFG}/dir_exists.obj ${CFG}/sqlite3.obj
${CFG}/RJM_SQLite_Resultset.obj ${CFG}/RJMFTime.obj
ar cq $@ ${CFG}/RJMFTime.obj
ar cq $@ ${CFG}/RJM_SQLite_Resultset.obj
ar cq $@ ${CFG}/sqlite3.obj
${CFG}/sqlite3.obj: ${CFG}/dir_exists.obj sqlite3.c
gcc -c sqlite3.c -o ${CFG}/sqlite3.obj -DTHREADSAFE=1
${CFG}/RJM_SQLite_Resultset.obj: ${CFG}/dir_exists.obj
RJM_SQLite_Resultset.cpp RJM_SQLite_Resultset.h
Glob_Defs.h RJMFTime.h sqlite3.h
$(CXX) -c RJM_SQLite_Resultset.cpp -o ${CFG}/RJM_SQLite_Resultset.obj
${CFG}/RJMFTime.obj: ${CFG}/dir_exists.obj RJMFTime.cpp RJMFTime.h
$(CXX) -c RJMFTime.cpp -o ${CFG}/RJMFTime.obj
${CFG}/dir_exists.obj:
-mkdir ${CFG}
-echo dir exists > ${CFG}/dir_exists.obj
clean:
-rm DEBUG/*.*
-rm RELEASE/*.*
-rmdir DEBUG
-rmdir RELEASE
注释
- Clean 节将删除 DEBUG 和 RELEASE 中的所有文件,并删除目录本身。它处理所有文件,这对于在备份之前清理目录非常有用。
- 如果你在更改 makefile 之前没有执行 make clean,你可能需要从 /c/code/sqlite_hello_world/ 中删除 .obj、.lib 和 .exe 文件(你可能还想删除 datafile.sqlite)。
- 有一个假的目录文件: dir_exists.obj。这允许 Make 在目录不存在时创建它。
解释
基本上,我所做的就是在对象文件前面添加了 ${CFG}。这意味着文件将在相关目录中创建。我还使所有文件都依赖于 ${CFG}/dir_exists.obj。这意味着如果目录不存在,它将被创建。你还可以看到一个增强的 Clean 节来处理子目录。
修复 Notepad++ bat 文件以使其正常工作
当我设置工具链时,我向 Notepad++ 菜单添加了菜单项。这会自动启动程序。现在这将不起作用,因为程序位于不同的位置。要解决此问题,请按照以下说明操作:
- "/c/program files/notepad++/notepad++.exe" /c/code/run.bat
- 粘贴以下代码,然后保存并关闭 Notepad++
:##BATCH to run fron notepad++
c:
cd\
cd %1
make
pause
cd DEBUG
main.exe
pause
此默认菜单选项将自动运行代码的 DEBUG 版本。你应该注意,此示例中使用的数据文件也存储在 DEBUG 目录中,并且在程序重建时总是重新创建。此行为是程序特定的,如果你希望保留数据文件,则需要将它们放置在其他位置。
最后,我创建了一个新菜单来生成 Release 构建。
- "/c/program files/notepad++/notepad++.exe" /c/code/run_release.bat
- 粘贴以下代码,然后保存并关闭 Notepad++
:##BATCH to run fron notepad++
c:
cd\
cd %1
make -e CFG=RELEASE
pause
cd RELEASE
main.exe
pause
- 加载 Notepad++
- 按 F5
- 输入 c:\code\run_release.bat $(CURRENT_DIRECTORY)
- 将命令保存为 RUN RELEASE CODE
Clean 会同时清理 RELEASE 和 DEBUG,因此此处没有理由添加它。
自己动手
现在我们有了一种更简单、更清晰的方式来构建我们的程序,将源文件与创建的文件分开。你现在应该能够创建遵循此系统的新项目。在构建 makefile 时需要记住以下几点:
- 包含基本的起始部分,设置
${CFG}
和${CXX}
变量 - 使所有对象依赖于 ${CFG}/dir_exists.obj
- 包含构建 ${CFG}/dir_exists.obj 的说明
- 使 Clean 部分处理子目录
如果遵循这些指南,你可以轻松地创建遵循此系统的项目。
最后评论
Make 非常强大,仅使用我描述的功能,你就可以生成复杂的构建过程。我以前做过的一件事是使用 Make 构建程序,然后运行它。我编写的程序生成了源代码,然后我使用 Make 将其构建到我的主项目中。通过这种方式,我实现了自动生成的代码直接包含在构建过程中。
有些人说 Make 复杂且耗时。我不认为我在这里解释的太复杂,特别是因为这里的人无论如何都是程序员,变量等应该是第二天性。我个人喜欢控制构建过程并手动设置依赖项的想法,而不是让 IDE 来做。这也有用,因为它让我接触到我使用的库中常用的编译器指令,并且我可以看到发生了什么。当然,还有其他方法可用,你可能会觉得其他方法更好。即使你转向其他方法,你可能会很高兴你花了这段时间详细研究 Make 和构建过程。
链接
你可以在 https://gnu.ac.cn/software/make/manual/ 查看 Make 手册。
历史
- 2009 年 7 月 4 日 - 第一版。