Makefile - 快速指南


为什么是Makefile?

编译源代码文件可能会很累,尤其是当您必须包含多个源文件并每次需要编译时都键入编译命令时。Makefile 是简化此任务的解决方案。

Makefile 是特殊格式的文件,可帮助自动构建和管理项目。

例如,假设我们有以下源文件。

  • 主程序
  • 你好.cpp
  • 阶乘.cpp
  • 函数.h

主程序

以下是 main.cpp 源文件的代码 -

#include <iostream>

using namespace std;

#include "functions.h"

int main(){
   print_hello();
   cout << endl;
   cout << "The factorial of 5 is " << factorial(5) << endl;
   return 0;
}

你好.cpp

下面给出的代码是 hello.cpp 源文件 -

#include <iostream>

using namespace std;

#include "functions.h"

void print_hello(){
   cout << "Hello World!";
}

阶乘.cpp

Factorial.cpp 的代码如下 -

#include "functions.h"

int factorial(int n){
   
   if(n!=1){
      return(n * factorial(n-1));
   } else return 1;
}

函数.h

以下是 fnctions.h 的代码 -

void print_hello();
int factorial(int n);

编译文件并获取可执行文件的简单方法是运行命令 -

gcc  main.cpp hello.cpp factorial.cpp -o hello

此命令生成hello二进制文件。在这个例子中,我们只有四个文件,并且我们知道函数调用的顺序。因此,输入上述命令并准备最终的二进制文件是可行的。

然而,对于拥有数千个源代码文件的大型项目,维护二进制构建变得很困难。

make命令允许您管理大型程序或程序组。当您开始编写大型程序时,您会注意到重新编译大型程序比重新编译短程序需要更长的时间。此外,您注意到您通常只处理程序的一小部分(例如单个函数),而其余程序的大部分都没有改变。

在接下来的部分中,我们将了解如何为我们的项目准备 makefile。

Makefile - 宏

make程序允许您使用类似于变量的宏。宏在 Makefile 中定义为 = 对。下面显示了一个示例 -

MACROS  = -me
PSROFF  = groff -Tps
DITROFF = groff -Tdvi
CFLAGS  = -O -systype bsd43
LIBS    = "-lncurses -lm -lsdl"
MYFACE  = ":*)"

特殊宏

在目标规则集中发出任何命令之前,有一些预定义的特殊宏 -

  • $@ 是要创建的文件的名称。

  • $?是变更后的家属的姓名。

例如,我们可以使用如下规则 -

hello: main.cpp hello.cpp factorial.cpp
   $(CC) $(CFLAGS) $? $(LDFLAGS) -o $@

Alternatively:

hello: main.cpp hello.cpp factorial.cpp
   $(CC) $(CFLAGS) $@.cpp $(LDFLAGS) -o $@

在此示例中,$@ 代表hello,$? 或 $@.cpp 选取所有已更改的源文件。

隐式规则中还使用了两个比较特殊的宏。他们是 -

  • $< 导致该操作的相关文件的名称。

  • $* 目标文件和依赖文件共享的前缀。

常见的隐式规则是从 .cpp(源文件)构建 .o(对象)文件。

.cpp.o:
   $(CC) $(CFLAGS) -c $<

Alternatively:

.cpp.o:
   $(CC) $(CFLAGS) -c $*.c

常规宏

有各种默认宏。您可以通过输入“make -p”来打印默认值来查看它们。从它们的使用规则来看,大多数都是非常明显的。

这些预定义变量,即隐式规则中使用的宏,分为两类。它们如下 -

  • 作为程序名称的宏(例如 CC)

  • 包含程序参数的宏(例如 CFLAGS)。

下表列出了在 makefile 的内置规则中用作程序名称的一些常见变量 -

先生编号 变量和描述
1

增强现实

档案维护程序;默认是“ar”。

2

作为

编译汇编文件的程序;默认为“as”。

3

抄送

编写C程序的程序;默认为“cc”。

4

一氧化碳

从 RCS 检出文件的程序;默认为“co”。

5

CXX

编写C++程序的程序;默认是“g++”。

6

消费者保护计划

编程运行 C 预处理器,并将结果输出到标准输出;默认为“$(CC) -E”。

7

FC

编译或预处理 Fortran 和 Ratfor 程序的程序;默认为“f77”。

8

得到

从 SCCS 中提取文件的程序;默认是“获取”。

9

莱克斯

用于将 Lex 语法转换为源代码的程序;默认是“lex”。

10

亚克力

用于将 Yacc 语法转换为源代码的程序;默认是“yacc”。

11

皮棉

用于在源代码上运行 lint 的程序;默认为“lint”。

12

M2C

用于编译 Modula-2 源代码的程序;默认为“m2c”。

13

个人电脑

编译 Pascal 程序的程序;默认是“电脑”。

14

制造信息

将Texinfo源文件转换为Info文件的程序;默认是“makeinfo”。

15

泰克斯

从 TeX 源代码制作 TeX dvi 文件的程序;默认为“tex”。

16

TEXI2DVI

从 Texinfo 源制作 TeX dvi 文件的程序;默认为“texi2dvi”。

17 号

编织

将 Web 翻译成 TeX 的程序;默认为“编织”。

18

编织

将 C Web 翻译成 TeX 的程序;默认为“cweave”。

19

纠纷

将 Web 翻译成 Pascal 的程序;默认为“缠结”。

20

唐格

将C Web 翻译成C 的程序;默认为“矩形”。

21

R M

删除文件的命令;默认为“rm -f”。

这是一个变量表,其值是上述程序的附加参数。除非另有说明,所有这些的默认值都是空字符串。

先生。 变量和描述
1

ARFLAGS

给档案维护程序的标志;默认是“rv”。

2

ASFLAG

在“.s”或“.S”文件上显式调用时提供给汇编器的额外标志。

3

CFLAGS

提供给 C 编译器的额外标志。

4

CXX标志

提供给 C 编译器的额外标志。

5

联合标志

给予 RCS 联合程序的额外标志。

6

CPPF标志

给予 C 预处理器和使用它的程序(例如 C 和 Fortran 编译器)的额外标志。

7

FFLAGS

提供给 Fortran 编译器的额外标志。

8

玻璃纤维标记

提供给 SCCS get 程序的额外标志。

9

LDFLAG

当编译器应该调用链接器“ld”时,为编译器提供额外的标志。

10

LFLAGS

给 Lex 的额外标志。

11

Y标志

给 Yacc 的额外标志。

12

PFLAGS

提供给 Pascal 编译器的额外标志。

13

射频标记

为 Ratfor 程序提供给 Fortran 编译器的额外标志。

14

林特旗

给予 lint 的额外标志。

注意- 您可以使用“-R”或“--no-builtin-variables”选项取消隐式规则使用的所有变量。

您还可以在命令行定义宏,如下所示 -

make CPP = /home/courses/cop4530/spring02

在 Makefile 中定义依赖关系

最终的二进制文件依赖于各种源代码和源头文件是很常见的。依赖关系很重要,因为它们让make知道任何目标的源。考虑以下示例 -

hello: main.o factorial.o hello.o
   $(CC) main.o factorial.o hello.o -o hello

在这里,我们告诉make hello 依赖于 main.o、factorial.o 和 hello.o 文件。因此,只要这些目标文件发生更改,make就会采取行动。

同时,我们需要告诉make如何准备.o文件。因此,我们还需要定义这些依赖关系,如下所示 -

main.o: main.cpp functions.h
   $(CC) -c main.cpp

factorial.o: factorial.cpp functions.h
   $(CC) -c factorial.cpp

hello.o: hello.cpp functions.h
   $(CC) -c hello.cpp

在 Makefile 中定义规则

我们现在将学习 Makefile 的规则。

Makefile 目标规则的一般语法是 -

target [target...] : [dependent ....]
[ command ...]

在上面的代码中,括号中的参数是可选的,省略号表示一个或多个。此处,请注意,每个命令前面的选项卡是必需的。

下面给出了一个简单的示例,您可以在其中定义一条规则,以从其他三个文件中创建目标 hello。

hello: main.o factorial.o hello.o
   $(CC) main.o factorial.o hello.o -o hello

注意- 在此示例中,您必须给出规则以从源文件创建所有目标文件。

语义非常简单。当你说“make target”时,make会找到适用的目标规则;并且,如果任何依赖项比目标新,则 make一次执行一个命令(在宏替换之后)。如果必须创建任何依赖项,则首先发生(因此您有一个递归)。

如果任何命令返回失败状态,Make将终止。在这种情况下将显示以下规则 -

clean:
   -rm *.o *~ core paper

Make忽略以破折号开头的命令行上返回的状态。例如,谁在乎没有 core 文件呢?

在宏替换后, Make会回显命令,以向您显示正在发生的情况。有时您可能想将其关闭。例如 -

install:
   @echo You must be root to install

人们已经开始期待 Makefile 中的某些目标。您应该始终先浏览。但是,可以合理地预期找到 all(或只是 make)、install 和 clean 目标。

  • make all - 它编译所有内容,以便您可以在安装应用程序之前进行本地测试。

  • make install - 它在正确的位置安装应用程序。

  • make clean - 它清理应用程序,删除可执行文件,任何临时文件,目标文件等。

Makefile 隐式规则

该命令应该适用于我们从源代码 x.cpp 构建可执行文件 x 的所有情况。这可以说是一个隐含的规则 -

.cpp:
   $(CC) $(CFLAGS) $@.cpp $(LDFLAGS) -o $@

这个隐式规则说明了如何从 xc 中生成 x——在 xc 上运行 cc 并调用输出 x。该规则是隐含的,因为没有提及特定目标。它可以在所有情况下使用。

另一个常见的隐式规则是从 .cpp(源文件)构建 .o(对象)文件。

.cpp.o:
   $(CC) $(CFLAGS) -c $<

alternatively

.cpp.o:
   $(CC) $(CFLAGS) -c $*.cpp

在 Makefile 中定义自定义后缀规则

Make可以自动创建ao文件,对相应的.c文件使用cc -c。这些规则内置于make中,您可以利用这一优势来缩短 Makefile。如果您仅在 Makefile 的依赖行中指定当前目标所依赖的 .h 文件,则make会知道相应的 .c 文件已经是必需的。您不必包含编译器的命令。

这进一步减少了 Makefile,如下所示 -

OBJECTS = main.o hello.o factorial.o
hello: $(OBJECTS)
   cc $(OBJECTS) -o hello
hellp.o: functions.h

main.o: functions.h 
factorial.o: functions.h 

Make使用一个名为.SUFFIXES的特殊目标来允许您定义自己的后缀。例如,请参考下面给出的依赖行 -

.SUFFIXES: .foo .bar

它通知make您将使用这些特殊后缀来制定您自己的规则。

make已经知道如何从.c文件创建.o文件类似,您可以按以下方式定义规则 -

.foo.bar:
   tr '[A-Z][a-z]' '[N-Z][A-M][n-z][a-m]' < $< > $@
.c.o:
   $(CC) $(CFLAGS) -c $<

第一条规则允许您从.foo文件创建.bar文件。它基本上会打乱文件。第二条规则是make用于从.c文件创建.o文件的默认规则。

Makefile - 指令

有多种不同形式的指令可供使用。您系统上的make程序可能不支持所有指令因此,请检查您的品牌是否支持我们在此解释的指令。GNU make支持这些指令。

有条件指令

条件指令是 -

  • ifeq指令开始条件并指定条件。它包含两个参数,用逗号分隔并用括号括起来。对两个参数执行变量替换,然后对它们进行比较。如果两个参数匹配,则遵循 ifeq 后面的 makefile 行;否则它们将被忽略。

  • ifneq指令开始条件并指定条件。它包含两个参数,用逗号分隔并用括号括起来。对两个参数执行变量替换,然后对它们进行比较。如果两个参数不匹配,则遵循 ifneq 后面的 makefile 行;否则它们将被忽略。

  • ifdef指令开始条件并指定条件。它包含单个参数。如果给定的参数为真,则条件为真。

  • ifndef指令开始条件并指定条件。它包含单个参数。如果给定的参数为假,则条件为真。

  • 如果前面的条件失败,则else指令会导致遵循以下行。在上面的示例中,这意味着只要不使用第一个替代链接命令,就使用第二个替代链接命令。条件语句中是否有 else 是可选的。

  • endif指令结束条件。每个条件语句都必须以 endif 结尾。

条件指令的语法

没有 else 的简单条件的语法如下 -

conditional-directive
   text-if-true
endif

text-if-true 可以是任何文本行,如果条件为 true,则将其视为 makefile 的一部分。如果条件为假,则不使用任何文本。

复杂条件的语法如下 -

conditional-directive
   text-if-true
else
   text-if-false
endif

如果条件为 true,则使用 text-if-true;否则,使用 text-if-false。text-if-false 可以是任意数量的文本行。

无论条件是简单还是复杂,条件指令的语法都是相同的。有四种不同的指令可以测试不同的条件。它们是给定的 -

ifeq (arg1, arg2)
ifeq 'arg1' 'arg2'
ifeq "arg1" "arg2"
ifeq "arg1" 'arg2'
ifeq 'arg1' "arg2" 

上述条件的相反指令如下:

ifneq (arg1, arg2)
ifneq 'arg1' 'arg2'
ifneq "arg1" "arg2"
ifneq "arg1" 'arg2'
ifneq 'arg1' "arg2" 

条件指令示例

libs_for_gcc = -lgnu
normal_libs =

foo: $(objects)
ifeq ($(CC),gcc)
   $(CC) -o foo $(objects) $(libs_for_gcc)
else
   $(CC) -o foo $(objects) $(normal_libs)
endif

包含指令

include指令允许make暂停读取当前 makefile 并在继续之前读取一个或多个其他 makefile。该指令是 makefile 中的一行,如下所示:

include filenames...

文件名可以包含 shell 文件名模式。行首允许并忽略额外的空格,但不允许使用制表符。例如,如果您有三个“.mk”文件,即“a.mk”、“b.mk”和“c.mk”,以及 $(bar),那么它会扩展为 bish bash,然后是以下内容表达。

include foo *.mk $(bar)

is equivalent to:

include foo a.mk b.mk c.mk bish bash

make处理 include 指令时,它会暂停读取 makefile 并依次读取每个列出的文件。完成后,make继续读取该指令所在的 makefile。

覆盖指令

如果使用命令参数设置了变量,则 makefile 中的普通赋值将被忽略。如果您想在 makefile 中设置变量,即使它是使用命令参数设置的,您也可以使用 override 指令,该指令如下所示:

override variable = value

or

override variable := value

Makefile - 重新编译

make程序是一个智能实用程序,它根据您在源文件中所做的更改来工作如果有四个文件main.cpp、hello.cpp、factorial.cpp和functions.h,那么其余所有文件都依赖于functions.h,并且main.cpp同时依赖于hello.cpp和factorial.cpp。因此,如果您对functions.h进行任何更改,则make会重新编译所有源文件以生成新的目标文件。但是,如果您在 main.cpp 中进行任何更改,因为这不依赖于任何其他文件,则只有 main.cpp 文件会被重新编译,而 help.cpp 和 Factorial.cpp 则不会。

编译文件时,make检查其目标文件并比较时间戳。如果源文件的时间戳比目标文件更新,则假设源文件已更改,它会生成新的目标文件。

避免重新编译

可能有一个由数千个文件组成的项目。有时您可能更改了源文件,但您可能不想重新编译依赖于它的所有文件。例如,假设您将一个宏或声明添加到其他文件所依赖的头文件中。保守地说,make假设头文件中的任何更改都需要重新编译所有依赖文件,但您知道它们不需要重新编译,并且您不想浪费时间等待它们编译。

如果您在更改头文件之前就预见到了问题,则可以使用“-t”标志。该标志告诉make不要运行规则中的命令,而是通过更改目标的最后修改日期来将目标标记为最新。您需要遵循此程序 -

  • 使用命令“make”重新编译真正需要重新编译的源文件。

  • 在头文件中进行更改。

  • 使用命令“make -t”将所有目标文件标记为最新。下次运行 make 时,头文件中的更改不会导致任何重新编译。

如果在某些文件确实需要重新编译时您已经更改了头文件,那么这样做就太晚了。相反,您可以使用“-o file”标志,它将指定文件标记为“旧”。这意味着,文件本身不会被重新制作,并且其帐户上的任何其他内容都不会被重新制作。你需要遵循这个程序 -

  • 使用“make -o header file”重新编译因与特定头文件无关的原因而需要编译的源文件。如果涉及多个头文件,请为每个头文件使用单独的“-o”选项。

  • 使用“make -t”更新所有目标文件。

Makefile - 其他功能

在本章中,我们将研究 Makefile 的各种其他功能。

递归使用 Make

递归使用make意味着在 makefile 中使用make作为命令。当您需要为构成较大系统的各个子系统使用单独的 makefile 时,此技术非常有用。例如,假设您有一个名为“subdir”的子目录,它有自己的 makefile,并且您希望包含目录的 makefile在该子目录上运行make 。您可以通过编写以下代码来做到这一点 -

subsystem:
   cd subdir && $(MAKE)

or, equivalently:
 	
subsystem:
   $(MAKE) -C subdir

您只需复制此示例即可编写递归make命令。但是,您需要了解它们的工作原理和原因,以及子品牌如何与顶级品牌相关。

将变量传递给子品牌

顶级make的变量值可以通过显式请求通过环境传递给子make。这些变量在子 make 中定义为默认值。除非使用“-e”开关,否则不能覆盖子 make makefile 使用的 makefile 中指定的内容。

要传递或导出变量,make将变量及其值添加到运行每个命令的环境中。子 make 反过来使用环境来初始化其变量值表。

特殊变量 SHELL 和 MAKEFLAGS 始终被导出(除非您取消导出它们)。如果您将其设置为任何值,则 MAKEFILES 会被导出。

如果要将特定变量导出到子 make,请使用导出指令,如下所示 -

export variable ...

如果要阻止导出变量,请使用 unexport 指令,如下所示 -

unexport variable ...

变量 MAKEFILES

如果定义了环境变量 MAKEFILES,make会将其值视为要在其他 makefile 之前读取的其他 makefile 的名称列表(以空格分隔)。这与 include 指令非常相似:在各个目录中搜索这些文件。

MAKEFILES 的主要用途是在make的递归调用之间进行通信。

包含来自不同目录的头文件

如果您将头文件放在不同的目录中并且在不同的目录中运行make,则需要提供头文件的路径。这可以使用 makefile 中的 -I 选项来完成。假设functions.h文件在/home/tutorialspoint/header文件夹中可用,其余文件在/home/tutorialspoint/src/文件夹中可用,那么makefile将编写如下 -

INCLUDES = -I "/home/tutorialspoint/header"
CC = gcc
LIBS =  -lm
CFLAGS = -g -Wall
OBJ =  main.o factorial.o hello.o

hello: ${OBJ}
   ${CC} ${CFLAGS} ${INCLUDES} -o $@ ${OBJS} ${LIBS}
.cpp.o:
   ${CC} ${CFLAGS} ${INCLUDES} -c $<

将更多文本附加到变量

通常,向已定义的变量的值添加更多文本很有用。您可以使用包含“+=”的行来执行此操作,如下所示 -

objects += another.o

它获取变量对象的值,并向其中添加文本“another.o”,前面有一个空格,如下所示。

objects = main.o hello.o factorial.o
objects += another.o

上面的代码将对象设置为“main.o hello.o Factorial.o another.o”。

使用“+=”类似于:

objects = main.o hello.o factorial.o
objects := $(objects) another.o

Makefile 中的续行

如果您不喜欢 Makefile 中的行太大,那么您可以使用反斜杠“\”来换行,如下所示 -

OBJ =  main.o factorial.o \
   hello.o

is equivalent to

OBJ =  main.o factorial.o hello.o

从命令提示符运行 Makefile

如果您已经准备了名称为“Makefile”的Makefile,那么只需在命令提示符下编写make,它就会运行Makefile文件。但如果您为 Makefile 指定了任何其他名称,请使用以下命令 -

make -f your-makefile-name

Makefile - 示例

这是编译 hello 程序的 Makefile 的示例。该程序由三个文件main.cppFactorial.cpphello.cpp组成。

# Define required macros here
SHELL = /bin/sh

OBJS =  main.o factorial.o hello.o
CFLAG = -Wall -g
CC = gcc
INCLUDE =
LIBS = -lm

hello:${OBJ}
   ${CC} ${CFLAGS} ${INCLUDES} -o $@ ${OBJS} ${LIBS}

clean:
   -rm -f *.o core *.core

.cpp.o:
   ${CC} ${CFLAGS} ${INCLUDES} -c $<

现在您可以使用make构建您的程序hello。如果您发出命令make clean,那么它将删除当前目录中可用的所有目标文件和核心文件。