这是在 Linux 中使用 make 命令的完整初学者指南。
你会学到:
- make命令的目的
- make 命令的安装
- 为示例 C 项目创建和使用联编文件
什么是 make 实用程序?
make 实用程序是程序员最方便的实用程序之一。 它的主要目的是编译一个大中型的软件项目。 make 实用程序是如此有用和通用,以至于即使是 Linux内核使用它!
要了解 make 实用程序的用途,首先必须了解为什么需要它。
随着您的软件变得越来越广泛,您开始越来越依赖外部依赖项(即库)。 您的代码开始拆分为多个文件,天知道每个文件中包含什么。 编译每个文件并将它们合理地链接在一起以生成必要的二进制文件变得很复杂。
“但我可以为此创建一个 Bash 脚本!”
为什么是的,你可以! 给你更多的力量! 但是随着项目的增长,您必须处理增量重建。 您将如何一般地处理它,以便即使您的文件数量增加,逻辑也保持正确?
这全部由 make 实用程序处理。 因此,让我们不要重新发明轮子,看看如何安装和使用好 make 实用程序。
安装制作工具
make 实用程序已经在几乎所有 Linux 发行版的第一方存储库中可用。
要在 Debian、Ubuntu 及其衍生产品上安装 make,请使用 apt
包管理器是这样的:
sudo apt install make
安装 make Fedora 和基于 RHEL 的 Linux 发行版,使用 dnf
像这样的包管理器:
sudo dnf install make
要在 Arch Linux 及其衍生版本上安装 make,请使用 pacman
包管理器是这样的:
sudo pacman -Sy make
现在安装了 make 实用程序,您可以继续通过示例来理解它。
创建一个基本的 makefile
make 实用程序根据项目代码存储库顶级目录中的 makefile 中指定的指令编译代码。
下面是我的项目的目录结构:
$ tree make-tutorial
make-tutorial
└── src
├── calculator.c
├── greeter.c
├── main.c
└── userheader.h
1 directory, 4 files
下面是内容 main.c
源文件:
#include <stdio.h>
#include "userheader.h"
int main()
{
greeter_func();
printf("nAdding 5 and 10 together gives us '%d'.n", add(5, 10));
printf("Subtracting 10 from 32 results in '%d'.n", sub(10, 32));
printf("If 43 is multiplied with 2, we get '%d'.n", mul(43, 2));
printf("The result of dividing any even number like 78 with 2 is a whole number like '%f'.n", div(78, 2));
return 0;
}
接下来是内容 greeter.c
源文件:
#include <stdio.h>
#include "userheader.h"
void greeter_func()
{
printf("Hello, user! I hope you are ready for today's basic Mathematics class!n");
}
下面是内容 calculator.c
源文件:
#include <stdio.h>
#include "userheader.h"
int add(int a, int b)
{
return (a + b);
}
int sub(int a, int b)
{
if (a > b)
return (a - b);
else if (a < b)
return (b - a);
else return 0;
}
int mul(int a, int b)
{
return (a * b);
}
double div(int a, int b)
{
if (a > b)
return ((double)a / (double)b);
else if (a < b)
return ((double)b / (double)a);
else
return 0;
}
最后,下面是内容 userheader.h
头文件:
#ifndef USERHEADER_DOT_H
#define USERHEADER_DOT_H
void greeter_func();
int add(int a, int b);
int sub(int a, int b);
int mul(int a, int b);
double div(int a, int b);
#endif /* USERHEADER_DOT_H */
makefile 的基础知识
在我们创建一个基本的 makefile 之前,让我们看一下 makefile 的语法。 Makefile 的基本构建块由一个或多个“规则”和“变量”组成。
makefile 中的规则
让我们先来看看makefile 中的规则。 makefile 的规则具有以下语法:
target : prerequisites
recipe
...
- 一个
target
是将由 make 生成的文件的名称。 这些通常是目标文件,稍后用于将所有内容链接在一起。 - 一个
prerequisite
是生成目标所必需的文件。 这是您通常指定您的.c
,.o
和.h
文件。 - 最后,一个
recipe
是生成所需的一个或多个步骤target
.
makefile 中的宏/变量
在 C 和 C++ 中,一个基本的语言特性是变量。 它们允许我们存储我们可能想在很多地方使用的值。 这有助于我们在需要时使用相同的变量名。 一个额外的好处是,如果我们需要更改值,我们只需要进行一次更改。
同样,makefile 可以包含变量。 它们有时被称为宏。 在 Makefile 中声明变量的语法如下:
variable = value
一个变量和它持有的值由等号分隔(=
) 符号。 多个值之间用空格分隔。
通常,变量用于存储编译所需的各种项目。 假设您要启用运行时缓冲区溢出检测并为可执行文件启用完整的 ASLR; 这可以通过将所有编译器标志存储在一个变量中来实现,例如 CFLAGS
.
下面是一个演示:
CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
我们创建了一个名为 CFLAGS
(编译器标志)并在此处添加了我们所有的编译器标志。
要使用我们的变量,我们可以将它括在以美元符号开头的括号中,如下所示:
gcc $(CFLAGS) -c main.c
我们的 makefile 中的上述行将添加所有指定的编译器标志并编译 main.c
根据我们的要求归档。
自动变量
make 实用程序有一些自动变量来帮助进一步简化重复。 这些变量通常用于规则的配方中。
部分自动变量如下:
自动变量 | 意义 |
---|---|
[email protected] | 目标规则的名称。 通常用于指定输出文件名。 |
$< | 第一个先决条件的名称。 |
$? | 比目标更新的所有先决条件的名称。 即最近一次代码编译后修改过的文件 |
$^ | 所有先决条件的名称,它们之间有空格。 |
您可以在上找到自动变量的完整列表 GNU Make 的官方文档.
隐式变量
和上面提到的自动变量一样,make 也有一些有固定用途的变量。 正如我之前使用的 CFLAGS
用于存储编译器标志的宏/变量,还有其他具有假定用途的变量。
这不能被认为是“保留关键字”,而更像是命名变量的“普遍共识”。
这些约定变量如下:
隐式变量 | 描述 |
---|---|
路径 | 使实用程序等同于 Bash PATH 多变的。 路径由冒号分隔 (: ). 默认情况下为空。 |
作为 | 这是汇编程序。 默认是 as 汇编程序。 |
CC | 编译C文件的程序。 默认是 cc . (通常, cc 指着 gcc .) |
CXX | 编译C++文件的程序。 默认是 g++ 编译器。 |
CPP | 运行 C 预处理器的程序。 默认设置为 $(CC) -E . |
莱克斯 | 将词法语法转换为源代码的程序。 默认是 lex . (你应该改变这个 至 flex .) |
皮棉 | 对源代码进行 lint 的程序。 默认是 lint . |
R M | 删除文件的命令。 默认是 rm -f . (请高度重视!) |
旗帜 | 这包含汇编程序的所有标志。 |
CFLAGS | 这包含 C 编译器的所有标志(cc ). |
CXX标志 | 这包含 C++ 编译器的所有标志(g++ ). |
CPP标志 | 这包含 C 预处理器的所有标志。 |
。假 | 指定与文件名不同的目标。 一个 example 是“清洁”目标; 其中 clean 的值为 .PHONY |
makefile 中的注释
makefile 中的注释类似于 shell 脚本中的注释。 它们以井号/哈希符号 (#
) 和所述行的内容(在井号/哈希符号之后)被 make 实用程序视为注释并被忽略。
下面是一个 example 证明这一点:
CFLAGS = -D_FORTIFY_SOURCE=2 -fpie -Wl,-pie
# The '-D_FORTIFY_SOURCE=2' flag enables run-time buffer overflow detection
# The flags '-fpie -Wl,-pie' are for enabling complete address space layout randomization
makefile 的初稿
现在我已经描述了 makefile 元素的基本语法以及我的简单项目的依赖树,现在让我们编写一个非常简单的 Makefile 来编译我们的代码并将所有内容链接在一起。
让我们从设置开始 CFLAGS
, CC
和 VPATH
我们编译所必需的变量。 (这不是完整的 makefile。我们将逐步构建它。)
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src
完成后,让我们定义构建规则。 我将创建 3 个规则,每个 .c
文件。 我的可执行二进制文件将被调用 make_tutorial
但你可以随心所欲!
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src
make_tutorial : main.o calculator.o greeter.o
$(CC) $(CFLAGS) $? -o [email protected]
main.o : main.c
$(CC) $(CFLAGS) -c $? -o [email protected]
calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o [email protected]
greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o [email protected]
如您所见,我正在编译所有 .c
文件转换为目标文件(.o
) 并在最后将它们链接在一起。
当我们运行 make
命令,它将从第一条规则开始(make_tutorial
). 这个规则是创建一个同名的最终可执行二进制文件。 它每个都有 3 个先决条件目标文件 .c
文件。
之后的每个连续规则 make_tutorial
规则是从同名的源文件创建目标文件。 我能理解这感觉有多复杂。 因此,让我们分解这些自动变量和隐式变量中的每一个,并理解它们的含义。
$(CC)
: 调用 GNU C 编译器 (gcc
).$(CFLAGS)
: 一个隐式变量传递给我们的编译器标志-Wall
, ETC。$?
:比目标更新的所有先决条件文件的名称。 在规则中main.o
,$?
将扩大到main.c
如果main.c
修改后main.o
已经生成。[email protected]
: 这是目标名称。 我用它来省略两次键入规则名称。 在规则中main.o
,[email protected]
扩展为main.o
.
最后,选项 -c
和 -o
是 gcc
用于编译/汇编源文件的选项,无需分别链接和指定输出文件名。 您可以通过运行 man 1 gcc
命令在你的终端。
现在让我们尝试运行这个 makefile,希望它能在第一次尝试时运行!
$ make
gcc -Wall -Wextra -c src/main.c -o main.o
gcc -Wall -Wextra -c src/calculator.c -o calculator.o
gcc -Wall -Wextra -c src/greeter.c -o greeter.o
gcc -Wall -Wextra main.o calculator.o greeter.o -o make_tutorial
如果你仔细观察,编译的每一步都包含了我们在 CFLAGS
隐式变量。 我们还可以看到源文件是自动从“src”目录中获取的。 这是自动发生的,因为我们在 VPATH
隐式变量。
让我们尝试运行 make_tutorial
二进制并验证是否一切正常。
$ ./make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!
Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
改进 makefile
“有什么需要改进的吗?”
让我们运行 ls
命令你可以自己看看;)
$ ls --group-directories-first -1
src
calculator.o
greeter.o
main.o
Makefile
make_tutorial
你看到构建工件(目标文件)了吗? 是的,他们会让事情变得更糟。 让我们使用我们的构建目录来减少这种混乱。
下面是修改后的 makefile:
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build
make_tutorial : main.o calculator.o greeter.o
$(CC) $(CFLAGS) $? -o [email protected]
build/main.o : main.c
mkdir build
$(CC) $(CFLAGS) -c $? -o [email protected]
build/calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o [email protected]
build/greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o [email protected]
在这里,我做了一个简单的改变:我添加了 build/
生成目标文件的每个规则之前的字符串。 这会将每个目标文件放入“构建”目录中。 我还添加了“build”到 VPATH
多变的。
仔细看的话,我们的第一个编译目标是 make_tutorial
. 但迂腐地第一个不会是目标。 配方运行的第一个目标是 main.o
(更确切地说 build/main.o
). 因此,我在 main.o
目标。
如果我不创建“build”目录,我会收到以下错误:
$ make
gcc -Wall -Wextra -c src/main.c -o build/main.o
Assembler messages:
Fatal error: can't create build/main.o: No such file or directory
make: *** [Makefile:12: build/main.o] Error 1
现在我们已经修改了我们的 makefile,让我们删除当前的构建工件以及编译的二进制文件并重新运行 make 实用程序。
$ rm -v *.o make_tutorial
removed 'calculator.o'
removed 'greeter.o'
removed 'main.o'
removed 'make_tutorial'
$ make
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o make_tutorial
这编译完美! 如果仔细观察,我们已经在 VPATH
变量,使 make 实用程序可以在“build”目录中搜索我们的目标文件。
我们的源文件和头文件是从“src”目录中自动找到的,构建工件(目标文件)保存在内部并从“build”目录链接,正如我们预期的那样。
添加 .PHONY 目标
我们可以将这一改进更进一步。 让我们添加“make clean”和“make run”目标。
下面是我们最终的 makefile:
CFLAGS = -Wall -Wextra
CC = gcc
VPATH = src:build
build/bin/make_tutorial : main.o calculator.o greeter.o
mkdir build/bin
$(CC) $(CFLAGS) $? -o [email protected]
build/main.o : main.c
mkdir build
$(CC) $(CFLAGS) -c $? -o [email protected]
build/calculator.o : calculator.c
$(CC) $(CFLAGS) -c $? -o [email protected]
build/greeter.o : greeter.c
$(CC) $(CFLAGS) -c $? -o [email protected]
.PHONY = clean
clean :
rm -rvf build
.PHONY = run
run: make_tutorial
./build/bin/make_tutorial
关于构建目标的一切都是一样的,除了我指定我想要 make_tutorial
二进制可执行文件放在 build/bin/
目录。
然后,我设置 .PHONY
变量为 clean
指定 clean
不是 make 实用程序需要担心的文件。 这是……假的。 在下面 clean
目标,我指定必须删除的内容以“清理所有内容”。
我也这样做 run
目标。 如果您是 Rust 开发人员,您会喜欢这种模式。 像 cargo run
命令,我使用 make run
命令运行编译后的二进制文件。
为我们运行 make_tutorial
二进制,它必须存在。 所以我将它添加到先决条件中 run
目标。
让我们跑 make clean
先跑再跑 make run
直接地!
$ make clean
rm -rvf build
removed 'build/greeter.o'
removed 'build/main.o'
removed 'build/calculator.o'
removed 'build/bin/make_tutorial'
removed directory 'build/bin'
removed directory 'build'
$ make run
mkdir build
gcc -Wall -Wextra -c src/main.c -o build/main.o
gcc -Wall -Wextra -c src/calculator.c -o build/calculator.o
gcc -Wall -Wextra -c src/greeter.c -o build/greeter.o
mkdir build/bin
gcc -Wall -Wextra build/main.o build/calculator.o build/greeter.o -o build/bin/make_tutorial
./build/bin/make_tutorial
Hello, user! I hope you are ready for today's basic Mathematics class!
Adding 5 and 10 together gives us '15'.
Subtracting 10 from 32 results in '22'.
If 43 is multiplied with 2, we get '86'.
The result of dividing any even number like 78 with 2 is a whole number like '39.000000'.
如您所见,我们没有运行 make
命令首先编译我们的项目。 在运行 make run
, 编译被处理了。 让我们了解它是如何发生的。
在运行 make run
命令,make 实用程序首先查看 run
目标。 的先决条件 run
target 是我们编译的二进制文件。 所以我们的 make_tutorial
二进制文件首先被编译。
这 make_tutorial
有自己的先决条件,这些先决条件放在 build/
目录。 一旦编译了这些目标文件,我们的 make_tutorial
二进制被编译; 最后,Make 实用程序返回到 run
目标和二进制文件 ./build/bin/make_tutorial
被执行。
如此优雅,哇
结论
本文介绍了 makefile 的基础知识,make 实用程序所依赖的文件可简化软件存储库的编译。 这是通过从一个基本的 Makefile 开始并随着我们的需求增长来构建它来完成的。