Makefile - 快速指南



为什么使用 Makefile?

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

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

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

  • main.cpp
  • hello.cpp
  • factorial.cpp
  • functions.h

main.cpp

以下是 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;
}

hello.cpp

以下代码是 hello.cpp 源文件 -

#include <iostream>

using namespace std;

#include "functions.h"

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

factorial.cpp

factorial.cpp 的代码如下 -

#include "functions.h"

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

functions.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) [email protected] $(LDFLAGS) -o $@

在此示例中,$@ 表示 hello,而 $? 或 [email protected] 拾取所有已更改的源文件。

隐式规则中还有另外两个特殊的宏。它们是 -

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

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

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

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

Alternatively:

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

常规宏

有各种默认宏。您可以通过键入“make -p”来打印默认值。大多数从它们使用的规则中都很明显。

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

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

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

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

序号 变量和描述
1

AR

维护档案的程序;默认为 `ar`。

2

AS

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

3

CC

编译 C 程序的程序;默认为 `cc`。

4

CO

从 RCS 中检出文件的程序;默认为 `co`。

5

CXX

编译 C++ 程序的程序;默认为 `g++`。

6

CPP

运行 C 预处理器的程序,结果输出到标准输出;默认为 `$(CC) -E`。

7

FC

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

8

GET

从 SCCS 中提取文件的程序;默认为 `get`。

9

LEX

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

10

YACC

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

11

LINT

用于对源代码运行 lint 的程序;默认为 `lint`。

12

M2C

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

13

PC

编译 Pascal 程序的程序;默认为 `pc`。

14

MAKEINFO

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

15

TEX

从 TeX 源代码创建 TeX dvi 文件的程序;默认为 `tex`。

16

TEXI2DVI

从 Texinfo 源代码创建 TeX dvi 文件的程序;默认为 `texi2dvi`。

17

WEAVE

将 Web 转换为 TeX 的程序;默认为 `weave`。

18

CWEAVE

将 C Web 转换为 TeX 的程序;默认为 `cweave`。

19

TANGLE

将 Web 转换为 Pascal 的程序;默认为 `tangle`。

20

CTANGLE

将 C Web 转换为 C 的程序;默认为 `ctangle`。

21

RM

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

下表列出了值是上述程序的其他参数的变量。所有这些变量的默认值为空字符串,除非另有说明。

序号。 变量和描述
1

ARFLAGS

要提供给档案维护程序的标志;默认为 `rv`。

2

ASFLAGS

在显式调用 `.s` 或 `.S` 文件上的汇编程序时要提供的额外标志。

3

CFLAGS

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

4

CXXFLAGS

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

5

COFLAGS

要提供给 RCS co 程序的额外标志。

6

CPPFLAGS

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

7

FFLAGS

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

8

GFLAGS

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

9

LDFLAGS

当编译器应该调用链接器 `ld` 时要提供的额外标志。

10

LFLAGS

要提供给 Lex 的额外标志。

11

YFLAGS

要提供给 Yacc 的额外标志。

12

PFLAGS

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

13

RFLAGS

要提供给 Fortran 编译器以用于 Ratfor 程序的额外标志。

14

LINTFLAGS

要提供给 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 忽略以连字符开头的命令行上的返回状态。例如,谁在乎是否有核心文件?

Make 会回显命令(宏替换后),以向您显示正在发生的事情。有时您可能希望关闭它。例如 -

install:
   @echo You must be root to install

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

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

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

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

Makefile 隐式规则

该命令应该在所有情况下都能正常工作,我们从中构建可执行文件 x 的源代码为 x.cpp。这可以表示为隐式规则 -

.cpp:
   $(CC) $(CFLAGS) [email protected] $(LDFLAGS) -o $@

此隐式规则说明了如何根据 x.c 创建 x - 对 x.c 运行 cc 并将输出命名为 x。该规则是隐式的,因为没有提到特定目标。它可以在所有情况下使用。

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

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

alternatively

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

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

Make 可以自动创建 .o 文件,使用 cc -c 在相应的 .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 程序可能不支持所有指令。因此,请检查您的 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 可以是任何文本行,被视为 Makefile 的一部分;如果条件为假,则不使用任何文本。

复杂条件判断的语法如下所示:

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

如果条件为真,则使用 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 指令

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。

override 指令

如果一个变量已使用命令参数设置,则 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 文件' 标志,该标志将指定的文件标记为“旧的”。这意味着,文件本身不会重新生成,并且不会因其而重新生成其他任何内容。您需要遵循以下步骤:

  • 使用 `make -o 头文件' 重新编译由于独立于特定头文件的原因而需要编译的源文件。如果涉及多个头文件,请为每个头文件使用单独的 `-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

顶级 make 的变量值可以通过环境通过显式请求传递给子 make。这些变量在子 make 中被定义为默认值。除非您使用 `-e' 开关,否则您无法覆盖子 make 使用的 Makefile 中指定的变量。

为了传递或导出变量,make 将变量及其值添加到环境中,以便运行每个命令。然后,子 make 使用环境来初始化其变量值表。

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

如果您希望将特定变量导出到子 make,请使用 export 指令,如下所示:

export variable ...

如果您希望阻止变量被导出,请使用 unexport 指令,如下所示:

unexport variable ...

变量 MAKEFILES

如果定义了环境变量 MAKEFILES,则 make 会将其值视为要读取的其他 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

它获取变量 objects 的值,并在其前面添加文本 `another.o',前面带有一个空格,如下所示。

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

以上代码将 objects 设置为 `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,则它会删除当前目录中所有目标文件和核心文件。

广告