什么是Makefile,它如何工作?

使用这个方便的自动化工具,更高效地运行和编译你的程序。
638 位读者喜欢这篇文章。
Open Data Policy

Opensource.com

如果你想在某些文件更新时运行或更新一个任务,`make` 工具会非常有用。 `make` 工具需要一个文件 `Makefile` (或 `makefile`),它定义了一组要执行的任务。 你可能已经使用 `make` 从源代码编译程序。 大多数开源项目使用 `make` 来编译最终的可执行二进制文件,然后可以使用 `make install` 安装它。

在本文中,我们将使用基本和高级示例来探索 `make` 和 `Makefile`。 在开始之前,请确保 `make` 已安装在你的系统中。

基本示例

让我们从在终端上打印经典的 "Hello World" 开始。 创建一个空目录 `myproject`,其中包含一个文件 `Makefile`,内容如下:

say_hello:
        echo "Hello World"

现在通过在目录 `myproject` 中键入 `make` 来运行该文件。 输出将是:

$ make
echo "Hello World"
Hello World

在上面的例子中,`say_hello` 的行为类似于函数名,就像在任何编程语言中一样。 这被称为 *目标 (target)* 。 *先决条件 (prerequisites)* 或 *依赖项 (dependencies)* 紧随目标之后。 为了简单起见,我们在这个例子中没有定义任何先决条件。 命令 `echo "Hello World"` 称为 *配方 (recipe)* 。 *配方* 使用 *先决条件* 来创建 *目标* 。 目标、先决条件和配方一起构成一个 *规则 (rule)* 。

总而言之,下面是典型规则的语法:

target: prerequisites
<TAB> recipe

例如,目标可能是一个依赖于先决条件(源文件)的二进制文件。 另一方面,先决条件也可以是依赖于其他依赖项的目标。

final_target: sub_target final_target.c
	Recipe_to_create_final_target

sub_target: sub_target.c
	Recipe_to_create_sub_target

目标不一定是文件;它可以只是配方的名称,就像我们的例子一样。 我们称这些为“伪目标 (phony targets)”。

回到上面的例子,当执行 `make` 时,整个命令 `echo "Hello World"` 被显示出来,然后是实际的命令输出。 我们通常不希望这样。 要抑制回显实际命令,我们需要在 `echo` 前面加上 `@`

say_hello:
        @echo "Hello World"

现在再次尝试运行 `make` 。 输出应该只显示这个:

$ make
Hello World

让我们在 `Makefile` 中添加几个伪目标: `generate` 和 `clean`

say_hello:
        @echo "Hello World"

generate:
	@echo "Creating empty text files..."
	touch file-{1..10}.txt

clean:
	@echo "Cleaning up..."
	rm *.txt

如果我们在更改后尝试运行 `make`,则只会执行目标 `say_hello` 。 这是因为makefile中只有第一个目标是默认目标。 通常被称为 *默认目标 (default goal)* ,这就是你会在大多数项目中看到 `all` 作为第一个目标的原因。 `all` 的职责是调用其他目标。 我们可以使用一个特殊的伪目标 `.DEFAULT_GOAL` 来覆盖此行为。

让我们在makefile的开头包含它

.DEFAULT_GOAL := generate

这将运行目标 `generate` 作为默认目标。

$ make
Creating empty text files...
touch file-{1..10}.txt

顾名思义,伪目标 `.DEFAULT_GOAL` 一次只能运行一个目标。 这就是为什么大多数 makefile 都包含 `all` 作为可以根据需要调用尽可能多目标的目标。

让我们包含伪目标 `all` 并删除 `.DEFAULT_GOAL`

all: say_hello generate

say_hello:
	@echo "Hello World"

generate:
	@echo "Creating empty text files..."
	touch file-{1..10}.txt

clean:
	@echo "Cleaning up..."
	rm *.txt

在运行 `make` 之前,让我们包含另一个特殊的伪目标 `.PHONY`,我们在其中定义所有不是文件的目标。 无论具有该名称的文件是否存在或其上次修改时间是什么,`make` 都会运行其配方。 这是完整的makefile

.PHONY: all say_hello generate clean

all: say_hello generate

say_hello:
	@echo "Hello World"

generate:
	@echo "Creating empty text files..."
	touch file-{1..10}.txt

clean:
	@echo "Cleaning up..."
	rm *.txt

`make` 应该调用 `say_hello` 和 `generate`

$ make
Hello World
Creating empty text files...
touch file-{1..10}.txt

一个好的做法是在 `all` 中不调用 `clean` 或将其作为第一个目标。 当需要清理时,应该手动调用 `clean` 作为 `make` 的第一个参数

$ make clean
Cleaning up...
rm *.txt

现在你已经了解了基本makefile的工作原理以及如何编写一个简单的makefile,让我们看一些更高级的例子。

高级示例

变量

在上面的例子中,大多数目标和先决条件的值都是硬编码的,但在实际项目中,这些会被变量和模式替换。

在makefile中定义变量的最简单方法是使用 `=` 运算符。 例如,将命令 `gcc` 分配给变量 `CC`

CC = gcc

这也称为 *递归展开变量 (recursive expanded variable)* ,它在规则中使用,如下所示

hello: hello.c
    ${CC} hello.c -o hello

你可能已经猜到,当配方传递到终端时,它会展开如下:

gcc hello.c -o hello

`${CC}` 和 `$(CC)` 都是调用 `gcc` 的有效引用。 但是,如果有人尝试将变量重新分配给自己,则会导致无限循环。 让我们验证一下

CC = gcc
CC = ${CC}

all:
    @echo ${CC}

运行 `make` 会导致:

$ make
Makefile:8: *** Recursive variable 'CC' references itself (eventually).  Stop.

为了避免这种情况,我们可以使用 `:=` 运算符(这也称为 *简单展开变量 (simply expanded variable)*)。 我们应该可以毫无问题地运行下面的makefile

CC := gcc
CC := ${CC}

all:
    @echo ${CC}

模式和函数

以下makefile可以通过使用变量、模式和函数来编译所有 C 程序。 让我们逐行探索它

# Usage:
# make        # compile all binary
# make clean  # remove ALL binaries and objects

.PHONY = all clean

CC = gcc			# compiler to use

LINKERFLAG = -lm

SRCS := $(wildcard *.c)
BINS := $(SRCS:%.c=%)

all: ${BINS}

%: %.o
	@echo "Checking.."
	${CC} ${LINKERFLAG} $< -o $@

%.o: %.c
	@echo "Creating object.."
	${CC} -c $<

clean:
	@echo "Cleaning up..."
	rm -rvf *.o ${BINS}
  • 以 `#` 开头的行是注释。

  • 行 `.PHONY = all clean` 定义了伪目标 `all` 和 `clean`。

  • 变量 `LINKERFLAG` 定义了在配方中与 `gcc` 一起使用的标志。

  • `SRCS := $(wildcard *.c)`: `$(wildcard pattern)` 是 *文件名函数 (functions for filenames)* 之一。 在这种情况下,所有带有 `.c` 扩展名的文件都将存储在变量 `SRCS` 中。

  • `BINS := $(SRCS:%.c=%)`: 这被称为 *替换引用 (substitution reference)* 。 在这种情况下,如果 `SRCS` 的值为 `'foo.c bar.c'`,则 `BINS` 将具有 `'foo bar'`。

  • 行 `all: ${BINS}`:伪目标 `all` 将 ` ${BINS}` 中的值作为单个目标调用。

  • 规则

    %: %.o
      @echo "Checking.."
      ${CC} ${LINKERFLAG} $&lt; -o $@

    让我们看一个例子来理解这条规则。 假设 `foo` 是 ` ${BINS}` 中的值之一。 然后 `%` 将匹配 `foo`(`%` 可以匹配任何目标名称)。 下面是展开形式的规则

    foo: foo.o
      @echo "Checking.."
      gcc -lm foo.o -o foo

    如图所示,`%` 被 `foo` 替换。 `$<` 被 `foo.o` 替换。 `$<` 被模式化为匹配先决条件,而 `$@` 匹配目标。 将为 ` ${BINS}` 中的每个值调用此规则

  • 规则

    %.o: %.c
      @echo "Creating object.."
      ${CC} -c $&lt;

    先前规则中的每个先决条件都被认为是此规则的目标。 下面是展开形式的规则

    foo.o: foo.c
      @echo "Creating object.."
      gcc -c foo.c
  • 最后,我们删除了目标 `clean` 中的所有二进制文件和对象文件。

下面是上面makefile的重写,假设它位于具有单个文件 `foo.c` 的目录中:

# Usage:
# make        # compile all binary
# make clean  # remove ALL binaries and objects

.PHONY = all clean

CC = gcc			# compiler to use

LINKERFLAG = -lm

SRCS := foo.c
BINS := foo

all: foo

foo: foo.o
	@echo "Checking.."
	gcc -lm foo.o -o foo

foo.o: foo.c
	@echo "Creating object.."
	gcc -c foo.c

clean:
	@echo "Cleaning up..."
	rm -rvf foo.o foo

有关makefile的更多信息,请参阅 GNU Make manual,它提供了完整的参考和示例。

你还可以阅读我们的 GNU Autotools 简介,以了解如何自动生成编码项目的makefile。

标签
psachin
Sachin 热衷于自由和开源软件。 他是一位狂热的 GNU Emacs 用户,喜欢谈论和撰写关于开源、GNU/Linux、Git 和 Python 的文章。 他之前曾在 OpenStack、ManageIQ/CloudForms 和 Red Hat Insights 上工作过。 他也喜欢在业余时间探索 Swift 对象存储。 可以通过 IRC 联系到他,用户名是 psachin@{Libera.Chat, Freenode, OFTC, gnome}。

8 条评论

嗨,

感谢本教程。 非常有用且易于理解。

有趣的文章。 它激励我玩玩 Makefiles。

谢谢你

非常感谢!

非常感谢...
非常好的文章...

知识共享许可协议本作品根据知识共享署名 - 相同方式共享 4.0 国际许可协议获得许可。
© . All rights reserved.