make
make 是一种用于构建应用程序的实用程序。本教程将教你如何使用 Makefile 与此实用程序一起使用。本教程主要关注 GNU make。
你可能会认为 Make 仅仅是一个构建大型二进制文件或库的工具(它确实是,几乎到了缺点的地步),但它远不止于此。
Makefile 是机器可读的文档,使你的工作流程可重现。
– Mike Bostock , "为什么要使用 Make"[1]
在我们开始讨论“make”之前,让我们首先描述很久以前使用的构建过程:“手动编译”,也称为“手动构建”。如果你只有一个或两个源文件,这个过程并不是太糟糕。一个简单的编译过程可以通过手动输入以下代码来实现
g++ -o file file.cpp
对于初学者来说,这可能需要一段时间才能习惯。作为替代方案,使用 make 命令将是
make file
这两个命令都会做同样的事情——获取 file.cpp 源文件,编译它,然后将其链接到可执行文件中。
但问题在于用户必须使用多个源文件或依赖于库。创建一个 SDL 应用程序可能包含类似以下内容
g++ `sdl-config --cflags` -o file file.cpp
这越来越难记了,不是吗?
让我们添加一些优化
g++ -O3 -fomit-frame-pointer `sdl-config --cflags` -o file file.cpp
你真的想每次对源文件进行最细微的编辑时都输入整个命令行吗?这就是 make 的用武之地——它将节省你的时间!
一个 make 文件作为一个简单的依赖树工作——它编译过时的内容,然后将你的软件链接在一起。你将不得不指定编译选项,但它们不会再对你那么苛刻了。一个用于编译应用程序的简单 makefile
Makefile
default: myapp
myapp:
g++ -o file file.cpp
要编译你的应用程序,你可以执行
make
或者
make myapp
好吧,你现在可能会问,这有什么用?我自己可以为它创建一个 shell 脚本!所以现在让我们处理多个文件和目标文件——你的应用程序足够大了,可以链接了:它包含五个文件。
全局来看,存在三个函数
int main(); (file1.cpp) void file2function(std::string from); (file2.cpp) void file3function(std::string from); (file3.cpp)
File2 和 file3 有它们的头文件,因此它们可以相互链接。实际的应用程序将如下所示
file1.cpp
#include "file2.h"
#include "file3.h"
int main()
{
file2function("main");
file3function("main");
}
file2.h
#include <string>
#include <iostream>
#include "file3.h"
void file2function(std::string source);
file2.cpp
#include "file2.h"
void file2function(std::string from)
{
std::cout << "File 2 function was called from " << from << std::endl;
file3function("file2");
}
file3.h
#include <string>
#include <iostream>
#include "file2.h"
void file3function(std::string source);
file3.cpp
#include "file3.h"
void file3function(std::string from)
{
std::cout << "File 3 function was called from " << from << std::endl;
}
那么,我该如何将这一切链接起来呢?嗯,最基本的方法是手动完成所有操作
g++ -c -o file2.o file2.cpp
g++ -c -o file3.o file3.cpp
g++ -o file1 file1.cpp file2.o file3.o
但是,make 将提供一种更简单的方法,**Makefile**
Makefile
default: testcase
testcase: file2.o file3.o
g++ -o file1 file1.cpp file2.o file3.o
相当不同,对吧?有一种不同的方法来做到这一点,这就是使make如此优秀的原因
Makefile
# Create the executable "file1" from all the source files.
COMPILER=g++
OBJS=file1.o file2.o file3.o
default: testcase
testcase: $(OBJS)
$(COMPILER) -o file1 $(OBJS)
现在扩展起来有多容易?只需将一个文件添加到 OBJS 列表中,你就完成了!
这是一个完整的 makefile,它处理将多个源文件(并创建一个 bunch of ".o" 文件作为中间步骤)转换为最终的 "file1" 可执行文件的完整过程。
优秀的程序员在 makefile 中添加人类可读的注释(以“#”字符开头的行),以描述编写 makefile 的原因以及make打算如何使用此 makefile(可惜,这并不总是实际发生的情况)。
makefile 中的一些语句可能非常长。为了使它们更容易阅读,一些程序员将长语句分解成几个较短、更容易阅读的物理行。许多控制台显示 80 个字符宽,这是某些程序员用于分解行和注释的阈值。
简而言之,程序员可能会认为写几行较短的物理行来组成一个语句看起来更好,例如
some really long line \
with, like, \
a bajillion parameters and \
several file names \
and stuff
而不是将相同的语句压缩成一行物理行
some really long line with, like, a bajillion parameters and several file names and stuff
当你在命令提示符下键入“make clean”时,“make”会删除你指定的所有易于替换的文件——即从你的不可替换的手写源文件生成的中间文件和输出文件。
要告诉“make”某个文件是易于替换的文件,请将其添加到 makefile 中“clean”部分的“rm”行,并且每次你在命令提示符下键入“make clean”时,该文件都将被删除。(如果你的 makefile 中还没有“clean”部分,请在 makefile 的末尾添加一个如下所示的部分)
.PHONY: clean
clean:
rm *.o
rm file
在这个例子中,我们告诉“make”所有中间目标文件(“*.o”)和最终的可执行文件(“file”)都是安全的、易于重新生成的。通常,人们(或 automake 脚本)会设置 clean 步骤以删除 makefile 中的每个目标。[2]
通常,程序员会定期运行“make clean
”,然后存档目录中的每个文件,然后运行“make”以重新生成每个文件。然后他测试程序可执行文件,确信他正在测试的程序可以很容易地完全从存档中的文件重新生成。
许多 makefile 包含一个“make check
”目标,该目标执行自测试(如果有)。[3](make test
目标通常是 make check
的同义词)。[4]
为了支持测试优先编程和回归测试,“make check
”只运行测试,它不重建程序;但通常 make all
首先重新构建程序,然后也运行“make check”。
“make”接受三种设置变量的方式。
- 递归扩展变量 通过使用
variable = value
定义。这意味着如果一个变量 'y' 包含对另一个变量 'x' 的引用,并且 'x' 在 'y' 定义后发生更改,'y' 将包含对 'x' 所做的更改。
x = abc
y = $(x)ghi
x = abcdef
# x will be abcdef, while y will be abcdefghi
- 简单扩展变量 通过使用
variable := value
定义。这意味着如果一个变量 'x' 包含对另一个变量 'y' 的引用,则 'y' 变量将被扫描一次且仅一次。
x := abc
y := $(x)ghi
# x is 'abc' at this time
x := abcdef
# x will be abcdef, while y will be abcghi
- 如果你只想确保设置了一个变量,可以使用
variable ?= value
。如果变量已设置,则不会运行该定义;否则,'variable' 将设置为 'value'。[5]
许多程序员建议不要创建包含空格或美元符号的文件,以避免“make”和许多其他实用程序中的“文件名中包含空格”错误。[6]
如果您必须处理文件名中包含空格或美元符号的文件(例如文件“my $0.02 program.c”),有时您可以使用双反斜杠和双美元符号转义文件的字面名称——在 Makefile 中,该文件名可以表示为“my\\ $$0.02\\ program.c”。[7]
但是,当“make”处理文件列表(在内部表示为以空格分隔的文件名)时,双反斜杠转义不起作用。一种解决方法是使用无空格的表示形式(如“my+$$0.02+program.c”)来引用该文件名,然后在 makefile 中稍后使用 $(subst) 函数将该无空格表示形式转换回实际的磁盘文件名。[6] 但是,此解决方法无法处理包含空格和加号的文件名。
也许最简单的办法是避免使用包含空格的文件名作为“make”的输入或输出。
创建新的 makefile 时,请将其命名为“Makefile
”(8 个字符,无扩展名)。
原则上,您可以创建具有其他名称的 makefile,例如 makefile
或 GNUmakefile
,它们(像 Makefile
一样)会被 GNU 版本的 make
实用程序自动找到,或者您可以使用 --file
选项将其他名称传递给 make 实用程序。[8]
- ↑ Mike Bostock "为什么要使用 Make".2013.
- ↑ "Makefile 和 RMarkdown". 2015.
- ↑ "用户标准目标".
- ↑ "实现 `make check` 或 `make test`".
- ↑ "GNU Make 手册" “6.2 变量的两种类型”部分。
- ↑ a b John Graham-Cumming. "GNU Make 遇到文件名中包含空格". CM Crossroads 2007.
- ↑ John Graham-Cumming. "GNU Make 转义:在狂野中漫步" CM Crossroads 2007.
- ↑ "GNU Make 手册" “3.2 为您的 Makefile 指定什么名称”部分。
- GNU C 编程教程 - 编写 makefile
- Makefile 教程:如何编写 Makefile
- OPUS Makefile 教程
- C、gcc 和 gmake 的 Makefile 教程
- 使用 NMake
- 关于 GNU Make 的“询问 Make 先生”系列文章
- make 有什么问题?
- GNU make 有什么问题?
- Peter Miller. "递归 Make 被认为是有害的". (曾经是 递归 Make 被认为是有害的)
- 高级自动依赖项生成.
- "Kleknev:构建系统的粗粒度分析器". 有时候“make”用于构建极其庞大复杂的系统;Kleknev 系统可以帮助人们找出在构建过程中哪些项目消耗了最多的时间。