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

快速查看 Make

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.73/5 (8投票s)

2009年7月5日

CPOL

10分钟阅读

viewsIcon

18393

downloadIcon

167

快速了解 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.txtmake 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.txtfin1.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,但用户可以输入一个值。如果该值不是 DEBUGRELEASE,则构建过程会出错。你可以使用以下命令测试此 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 会同时清理 RELEASEDEBUG,因此此处没有理由添加它。

自己动手

现在我们有了一种更简单、更清晰的方式来构建我们的程序,将源文件与创建的文件分开。你现在应该能够创建遵循此系统的新项目。在构建 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 日 - 第一版。
© . All rights reserved.