admin 发表于 2025-1-21 21:46:09

一篇文章弄懂Makefile-最简明易懂的Makefile教程

# 起步

## Makefiles 简介

Makefiles 用于帮助决定一个大型程序的哪些部分需要重新编译。在绝大多数情况下,需要编译的只是 C 或 C++ 文件。其他语言通常有它们自己的一套与 Make 用途类似的工具。Make 的用途并不局限于编程,当你需要根据哪些文件发生了变更来运行一系列指令时也可以使用它。而本教程将只关注 C/C++ 编译用例。

这里有一个你可能会使用 Make 进行构建的依赖关系示例图。如有任何文件的依赖项发生了改变,那么该文件就会被重新编译。
!(data/attachment/forum/202501/21/215248nphhfgr539c50n8h.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/300 "dependency_graph.png")

## Make 的替代品

除了 Make,还有一些比较流行的构建系统可选,像 SCon、CMake、Bazel 和 Ninja 等。一些代码编辑器,像 Microsoft Visual Studio,内置了它们自己的构建工具。Java 语言的构建工具有 Ant、Maven和 Gradle 可选,其他语言像 Go 和 Rust 则都有它们自己的构建工具。

像 Python、Ruby 和 JavaScript 这样的解释型语言是不需要类似 Makefiles 的东西的。Makefiles 的目标是基于哪些文件发生了变化来编译需要被编译的一切文件。但是,当解释型语言的文件发生了变化,是不需要重新编译的,程序运行时会使用最新版的源码文件。

## Make 的不同实现与版本

虽然 Make 有多种实现,但本指南的大部分内容都是版本间无差异的。然而,需要说明的是,本指南是专门为 GNU Make 编写的,这也是 Linux 与 macOS 平台上的标准实现。文中所有的例子在 Make 的版本 3 和版本 4 上都能很好地工作,除了一些细微的差别外,它们的行为表现也几乎一致。

## 如何运行示例

为了运行文中示例,你需要一个安装了 `make` 的终端。对于每一个例子,只需要把它的内容放在一个名为 `Makefile` 的文件中,再把该文件放在运行 `make` 命令的目录下就行了。让我们从最简单的一个 Makefile 开始吧:

```makefile
hello:
    echo "hello world"
```

以下是运行上述示例的输出:

```shell
$ make
echo "hello world"
hello world
```

是的,就是这样!

## Makefile 语法

Makefile 文件由一系列的 _规则 (rules)_ 组成,一个规则类似下面这样:

```makefile
targets: prerequisites
    command
    command
    command
```

- `targets` 指的是文件名称,多个文件名以空格分隔。通常,一个规则只对应一个文件。
- `commands` 通常是一系列用于制作(make)一个或多个目标(targets)的步骤。它们 _需要以一个制表符开头_,而不是空格。
- `prerequisites` 也是文件名称,多个文件名以空格分隔。在运行目标(targets)的 `commands` 之前,要确保这些文件是存在的。它们也被称为 _依赖_。

## 新手示例

下面的 Makefile 有 3 个分离的 _规则 (rules)_。当你在终端运行 `make blah` 时,它会通过一系列的步骤构建一个名为 `blah` 的程序:

- `blah` 给 `make` 提供了构建目标(target)的名称,所以它会在 makefile 中优先被 `make` 程序搜索
- 构建系统发现 `blah` 依赖 `blah.o`,所以 `make` 开始搜索 `blah.o` 这个目标
- `blah.o` 又依赖 `blah.c`,所以 `make` 又开始搜索 `blah.c` 这个目标
- `blah.c` 没有依赖,直接运行 `echo` 命令
- 接着运行 `cc -c` 命令,因为 `blah.o` 的依赖的所有 `commands` 都执行完了
- 同理,接着运行顶部的 `cc` 命令
- 就这样,一个编译好的 C 程序 `blah` 就诞生了

```makefile
blah: blah.o
    cc blah.o -o blah # Runs third

blah.o: blah.c
    cc -c blah.c -o blah.o # Runs second

blah.c:
    echo "int main() { return 0; }" > blah.c # Runs first
```


`-c` 选项只编译不链接,`-o file` 将其前面命令的输出内容放在文件 _file_ 中。

详情可参阅:https://gcc.gnu.org/onlinedocs/gcc/Overall-Options.html#Overall-Options


下面的这个 makefile 只有一个目标,叫作 `some_file`。因为默认的目标就是第一个目标,所以,在执行 `make` 命令时,`some_file` 目标下的命令会运行。👇

```makefile
some_file:
    echo "This line will always print"
```

若应用下面这个 makefile,在第一次构建时将创建文件 *some_file*,第二次系统就会注意到该目标文件已经创建过了,结果就会得到这样的提示信息:*make: 'some_file' is up to date.*。👇

```makefile
some_file:
    echo "This line will only print once"
    touch some_file
```

敲黑板!目标 `some_file` 依赖 `other_file`。当我们运行 `make` 时,默认目标(即 `some_file`,因为它是第一个)会被“召唤”。构建系统首先查看目标的 _依赖_ 列表,若其中有旧的目标文件,构建系统首先会为这些依赖执行目标构建,此后才轮到默认目标。第二次执行 `make` 时,默认目标和依赖目标下的命令都不会再运行了,因为二者都存在了。👇

```makefile
some_file: other_file
    echo "This will run second, because it depends on other_file"
    touch some_file

other_file:
    echo "This will run first"
    touch other_file
```

而下面这个 makefile 中的两个目标每次构建都会运行,因为 `some_file` 依赖的 `other_file` 从未被创建过。👇

```makefile
some_file: other_file
    touch some_file

other_file:
    echo "nothing"
```


👆 类似上面 `other_file` 这样的目标就是俗称的 _伪目标_ 或 _虚拟目标_。


`clean` 经常被用来作为移除其他目标的输出的目标名称,但是在 `make` 看来它并非是一个特殊用词。

```makefile
some_file:
    touch some_file

clean:
    rm -f some_file
```

## 变量

在 Makefile 中,变量的值类型只能是字符串。下面是一个使用变量的例子:

```makefile
files = file1 file2
some_file: $(files)
    echo "Look at this variable: " $(files)
    touch some_file

file1:
    touch file1
file2:
    touch file2

clean:
    rm -f file1 file2 some_file
```

引用变量的语法是:`${}` 或 `$()`。

```makefile
x = dude

all:
    echo $(x)
    echo ${x}

    # Bad practice, but works
    echo $x
```

# 变量(第 2 部分)

## 变量类型和修改

变量的类型有两种:

- 递归变量(使用 `=`)- 只有在命令执行时才查找变量,而不是在定义时
- 简单的扩展变量(使用 `:=`)- 就像普通的命令式编程一样——只有当前已经定义的变量才会得到扩展

```makefile
# Recursive variable. This will print "later" below
one = one ${later_variable}
# Simply expanded variable. This will not print "later" below
two := two ${later_variable}

later_variable = later

all:
    echo $(one)
    echo $(two)
```

扩展变量(使用 `:=`)使得你可以在一个变量的基础上追加内容,递归变量则会陷入死循环。

```makefile
one = hello
# one gets defined as a simply expanded variable (:=) and thus can handle appending
one := ${one} there

all:
    echo $(one)
```

`?=` 用于当变量还没被设置值时给它设置值,反之则忽略。

```makefile
one = hello
one ?= will not be set
two ?= will be set

all:
    echo $(one)
    echo $(two)
```

变量值尾部的空格不会被删除,但开头的空格会被删除。想要一个值为单个空格的变量请使用 `$(nullstring)`。

```makefile
with_spaces = hello   # with_spaces has many spaces after "hello"
after = $(with_spaces)there

nullstring =
space = $(nullstring) # Make a variable with a single space.

all:
    echo "$(after)"
    echo start"$(space)"end
```

一个未定义的变量实际上是一个空字符串。

```makefile
all:
    # Undefined variables are just empty strings!
    echo $(nowhere)
```

`+=` 用来追加变量的值:

```makefile
foo := start
foo += more

all:
    echo $(foo)
```

## 命令行参数与覆盖

你可以通过 `override` 来覆盖来自命令行的变量。假使我们使用下面的 makefile 执行了这样一条命令 `make option_one=hi`,那么变量 `option_one` 的值就会被覆盖掉。

```makefile
# Overrides command line arguments
override option_one = did_override
# Does not override command line arguments
option_two = not_override
all:
    echo $(option_one)
    echo $(option_two)
```

## 命令列表与 `define`

`define` 实际上就是一个命令列表,它与函数 `define` 没有任何关系。这里请注意,它与用分号分隔多个命令的场景有点不同,因为前者如预期的那样,每条命令都是在一个单独的 shell 中运行的。

```makefile
one = export blah="I was set!"; echo $$blah

define two
export blah=set
echo $$blah
endef

# One and two are different.

all:
    @echo "This prints 'I was set'"
    @$(one)
    @echo "This does not print 'I was set' because each command runs in a separate shell"
    @$(two)
```

## 特定目标的变量

我们可以为特定目标分配变量。

```makefile
all: one = cool

all:
    echo one is defined: $(one)

other:
    echo one is nothing: $(one)
```

## 特定模式的变量

我们可以为特定的目标 _模式_ 分配变量。

```makefile
%.c: one = cool

blah.c:
    echo one is defined: $(one)

other:
    echo one is nothing: $(one)
```

# Makefiles 的条件判断

## if/else

```makefile
foo = ok

all:
ifeq ($(foo), ok)
    echo "foo equals ok"
else
    echo "nope"
endif
```

## 检查一个变量是否为空

```makefile
nullstring =
foo = $(nullstring) # end of line; there is a space here

all:
ifeq ($(strip $(foo)),)
    echo "foo is empty after being stripped"
endif
ifeq ($(nullstring),)
    echo "nullstring doesn't even have spaces"
endif
```

## 检查一个变量是否定义

`ifdef` 不会扩展变量引用,它只会查看变量的内容究竟定义没。

```makefile
bar =
foo = $(bar)

all:
ifdef foo
    echo "foo is defined"
endif
ifdef bar
    echo "but bar is not"
endif
```

## `$(makeflags)`

此示例向你展示了如何使用 `findstring` 和 `MAKEFLAGS` 测试 `make` 标志。使用 `make -i` 来运行此例看下输出如何吧。

```makefile
bar =
foo = $(bar)

all:
# Search for the "-i" flag. MAKEFLAGS is just a list of single characters, one per flag. So look for "i" in this case.
ifneq (,$(findstring i, $(MAKEFLAGS)))
    echo "i was passed to MAKEFLAGS"
endif
```

# 自动变量和通配符

## 通配符 `*`

在 Make 中,`%` 和 `*` 都叫作通配符,但是它们是两个完全不同的东西。`*` 会搜索你的文件系统来匹配文件名。我建议你应该一直使用 `wildcard` 函数来包裹它,要不然你可能会掉入下述的常见陷阱中。真是搞不明白,不用 `wildcard` 包裹的 `*` 除了能给人徒增迷惑,还有什么可取之处。

```makefile
# 打印出每个.c文件的文件信息
print: $(wildcard *.c)
      ls -la$?
```

`*` 可以用在 `targets`、`prerequisites` 以及 `wildcard` 函数中。
*`不能直接用在变量定义中。   当`*`匹配不到文件时,它将保持原样(除非被`wildcard` 函数包裹)。

```makefile
thing_wrong := *.o # 请不要这样做!'*.o' 将不会被替换为实际的文件名
thing_right := $(wildcard *.o)

all: one two three four

# 失败,因为$(thing_wrong)是字符串"*.o"
one: $(thing_wrong)

# 如果没有符合这个匹配规则的文件,它将保持为 *.o   :(
two: *.o

# 按预期运行!在这种情况下,什么都不会执行
three: $(thing_right)

# 与规则三相同
four: $(wildcard *.o)
```

## 通配符 `%`

通配符 `%` 虽然确实很有用,但是由于它的可用场景多种多样,着实有点让人摸不着头脑。

- 在“匹配”模式下使用时,它匹配字符串中的一个或多个字符,这种匹配被称为词干(stem)匹配。
- 在“替换”模式下使用时,它会替换匹配到的词干。
- `%` 大多用在规则定义以及一些特定函数中。

## 自动变量

虽然存在很多 自动变量,但是经常用到的没几个:

```makefile
hey: one two
      # 输出"hey",因为这是第一个目标
      echo $@

      # 输出所有比目标新的依赖
      echo $?

      # 输出所有依赖
      echo $^

      touch hey

one:
      touch one

two:
      touch two

clean:
      rm -f hey one two
```

# 函数

## 第 1 个函数

_函数_ 主要用于文本处理。函数调用的语法是 `$(fn, arguments)` 或 `${fn, arguments}`。你可以使用内置的函数 `call`) 来制作自己的函数。Make 拥有数量众多的 内置函数。

```makefile
bar := ${subst not, totally, "I am not superman"}
all:
    @echo $(bar)
```

如果你想替换空格或逗号,请使用变量:

```makefile
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space),$(comma),$(foo))

all:
    @echo $(bar)
```

**不要** 在第一个参数之后的参数中包含空格,这将被视为字符串的一部分。

```makefile
comma := ,
empty:=
space := $(empty) $(empty)
foo := a b c
bar := $(subst $(space), $(comma) , $(foo))

all:
    # Output is ", a , b , c". Notice the spaces introduced
    @echo $(bar)
```

## 字符串替换

`$(patsubst pattern,replacement,text)` 做了下面这些事:

> “在文本中查找匹配的以空格分隔的单词,用 `replacement` 替换它们。这里的 `pattern` 可以包含一个 `%` 作为通配符以匹配单词中任意数量的任意字符。如果 `replacement` 中也包含了一个 `%`,那它表示的内容将被 `pattern` 中的 `%` 匹配的内容替换。只有 `pattern` 和 `replacement` 中的第一个 `%` 才会采取这种行为,随后的任何 `%` 都将保持不变。

`$(text:pattern=replacement)` 是一个简化写法。

还有一个仅替换后缀的简写形式:`$(text:suffix=replacement)`,这里没有使用通配符 `%`。
在简写形式中,不要添加额外的空格,它会被当作一个搜索或替换项。

```makefile
foo := a.o b.o l.a c.o
one := $(patsubst %.o,%.c,$(foo))
# This is a shorthand for the above
two := $(foo:%.o=%.c)
# This is the suffix-only shorthand, and is also equivalent to the above.
three := $(foo:.o=.c)

all:
    echo $(one)
    echo $(two)
    echo $(three)
```

## 函数 `foreach`

函数 `foreach` 看起来像这样:`$(foreach var,list,text)`,它用于将一个单词列表(空格分隔)转换为另一个。`var` 表示循环中的每一个单词,`text` 用于扩展每个单词。

在每个单词后追加一个感叹号:

```makefile
foo := who are you
# For each "word" in foo, output that same word with an exclamation after
bar := $(foreach wrd,$(foo),$(wrd)!)

all:
    # Output is "who! are! you!"
    @echo $(bar)
```

## 函数 `if`

`if` 函数用来检查它的第 1 个参数是否非空。如果非空,则运行第 2 个参数,否则运行第 3 个。

```makefile
foo := $(if this-is-not-empty,then!,else!)
empty :=
bar := $(if $(empty),then!,else!)

all:
    @echo $(foo)
    @echo $(bar)
```

## 函数 `call`

Make 支持创建基本的函数。你只需通过创建变量来“定义”函数,只是会用到参数 `$(0)`、`$(1)` 等。然后,你就可以使用专门的函数 `call` 来调用它了,语法是 `$(call variable,param,param)`。`$(0)` 是变量名,而 `$(1)`、`$(1)` 等则是参数。

```makefile
sweet_new_fn = Variable Name: $(0) First: $(1) Second: $(2) Empty Variable: $(3)

all:
    # Outputs "Variable Name: sweet_new_fn First: go Second: tigers Empty Variable:"
    @echo $(call sweet_new_fn, go, tigers)
```

## 函数 `shell`

`shell` - 调用 shell,但它在输出中会用空格替代换行。

```makefile
all:
    @echo $(shell ls -la) # Very ugly because the newlines are gone!
```

# 命令与执行

## 回显/静默命令

在一个命令前添加一个 `@` 符号就会阻止该命令输出内容。

你也可以使用 `make -s` 在每个命令前添加 `@`。

```makefile
all:
    @echo "This make line will not be printed"
    echo "But this will"
```

## 命令的执行

每个命令都运行在一个新的 shell 中(或者说运行效果等同于运行在一个新 shell 中)。

```makefile
all:
    cd ..
    # The cd above does not affect this line, because each command is effectively run in a new shell
    echo `pwd`

    # This cd command affects the next because they are on the same line
    cd ..;echo `pwd`

    # Same as above
    cd ..; \
    echo `pwd`
```

## 默认的 Shell

系统默认的 shell 是 `/bin/sh`,你可以通过改变 `SHELL` 变量的值来改变它:

```makefile
SHELL=/bin/bash

cool:
    echo "Hello from bash"
```

## 错误处理:`-k`,`-i` 和 `-`

`make -k` 会使得即便遇到错误,构建也会继续执行下去。如果你想一次查看 Make 的所有错误,这会很有帮助。

在一个命令前添加 `-` 会抑制错误。

`make -i` 等同于在每个命令前添加 `-`。

```makefile
one:
    # This error will be printed but ignored, and make will continue to run
    -false
    touch one
```

## 中断或杀死 `make`

如果你在 `make` 的过程中,使用了 `ctrl+c`,那么刚刚制作的新目标会被删除。

## `make` 的递归用法

为了递归应用一个 makefile,请使用 `$(MAKE)` 而不是 `make`,因为它会为你传递构建标志,而使用了 `$(MAKE)` 变量的这一行命令不会应用这些标志。

```makefile
new_contents = "hello:\n\ttouch inside_file"
all:
    mkdir -p subdir
    printf $(new_contents) | sed -e 's/^ //' > subdir/makefile
    cd subdir && $(MAKE)

clean:
    rm -rf subdir
```

## 在递归 make 中使用 `export`

指令 `export` 携带了一个变量,并且对子 `make` 命令可见。在下面的例子中,变量 `cooly` 被导出以便在子目录中的 makefile 可以使用它。
`export` 的语法与 sh 相同,但二者并不相关(虽然功能类似)。

```makefile
new_contents = "hello:\n\\techo \$$(cooly)"

all:
    mkdir -p subdir
    echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
    @echo "---MAKEFILE CONTENTS---"
    @cd subdir && cat makefile
    @echo "---END MAKEFILE CONTENTS---"
    cd subdir && $(MAKE)

# Note that variables and exports. They are set/affected globally.
cooly = "The subdirectory can see me!"
export cooly
# This would nullify the line above: unexport cooly

clean:
    rm -rf subdir
```

在 shell 中运行的变量也需要导出。

```makefile
one=this will only work locally
export two=we can run subcommands with this

all:
    @echo $(one)
    @echo $$one
    @echo $(two)
    @echo $$two
```

`.EXPORT_ALL_VARIABLES` 可以为你导出所有变量。

```makefile
.EXPORT_ALL_VARIABLES:
new_contents = "hello:\n\techo \$$(cooly)"

cooly = "The subdirectory can see me!"
# This would nullify the line above: unexport cooly

all:
    mkdir -p subdir
    echo $(new_contents) | sed -e 's/^ //' > subdir/makefile
    @echo "---MAKEFILE CONTENTS---"
    @cd subdir && cat makefile
    @echo "---END MAKEFILE CONTENTS---"
    cd subdir && $(MAKE)

clean:
    rm -rf subdir
```

## 给 `make` 传递参数

看下 `--dry-run`,`--touch` 和 `--old-file` 选项吧。

你可以同时传递多个目标给 `make`,例如 `make clean run test` 会先后运行 `clean`、`run`、`test`。

# 各种规则

## 隐式规则

Make 钟爱 C 编译,它每次表达爱意时,都会做出一些“迷惑行为”。其中最令人迷惑的部分可能就是它的那些魔法般的规则了,Make 称之为“隐式规则”。我个人不认同这个设计方案,也不推荐使用它们。然而即便如此,它们也被经常使用,所以了解一下它们也是很有用处的。下面列出了隐式规则:

- 编译 C 程序时:使用 `$(CC) -c $(CPPFLAGS) $(CFLAGS)` 形式的命令,`n.o` 会由 `n.c` 自动生成。
- 编译 C++ 程序时:使用 `$(CXX) -c $(CPPFLAGS) $(CXXFLAGS)` 形式的命令,`n.o` 会由 `n.cc` 或 `n.pp` 自动生成。
- 链接单个目标文件时:通过运行 `$(CC) $(LDFLAGS) n.o $(LOADLIBES) $(LDLIBS)` 命令,`n` 会由 `n.o` 自动生成。

上述隐式规则使用的变量的含义如下所示:

- `CC`:编译 C 程序的程序,默认是 `cc`
- `CXX`:编译 C++ 程序的程序,默认是 `G++`
- `CFLAGS`:提供给 C 编译器的额外标志
- `CXXFLAGS`:提供给 C++ 编译器的额外标志
- `CPPFLAGS`:提供给 C 预处理器的额外标志
- `LDFLAGS`:当编译器应该调用链接器时提供给编译器的额外标志

现在就让我们来看一下如何在不明确告诉 Make 该如何进行编译的情况下构件一个 C 程序。

```makefile
CC = gcc # Flag for implicit rules
CFLAGS = -g # Flag for implicit rules. Turn on debug info

# Implicit rule #1: blah is built via the C linker implicit rule
# Implicit rule #2: blah.o is built via the C compilation implicit rule, because blah.c exists
blah: blah.o

blah.c:
    echo "int main() { return 0; }" > blah.c

clean:
    rm -f blah*
```

## 静态模式规则

静态模式规则是另一种可以在 Makefile 文件中“少废笔墨”的方式,但是我认为它更加有用且更容易让人理解。其语法如下:

```makefile
targets ...: target-pattern: prereq-patterns ...
   commands
```

它的本质是:给定的目标 `target` 由 `target-pattern` 在 `targets` 中匹配得到(利用通配符 `%`)。匹配到的内容被称为 _词干 (stem)_。然后,将词干替换到 `prereq-pattern` 中去,并以此生成目标的 `prerequisites` 部分。

静态模式规则的一个典型用例就是把 `.c` 文件编译为 `.o` 文件。下面是 _手动_ 方式:

```makefile
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
foo.o: foo.c
bar.o: bar.c
all.o: all.c

all.c:
    echo "int main() { return 0; }" > all.c

%.c:
    touch $@

clean:
    rm -f *.c *.o all
```

下面则是使用了静态模式的一种 _更加有效的方式_:

```makefile
objects = foo.o bar.o all.o
all: $(objects)

# These files compile via implicit rules
# Syntax - targets ...: target-pattern: prereq-patterns ...
# In the case of the first target, foo.o, the target-pattern matches foo.o and sets the "stem" to be "foo".
# It then replaces the '%' in prereq-patterns with that stem
$(objects): %.o: %.c

all.c:
    echo "int main() { return 0; }" > all.c

%.c:
    touch $@

clean:
    rm -f *.c *.o all
```

## 静态模式规则与过滤器

虽然函数稍后才会介绍到,但是我会预先向你展示你可以用函数来做什么。函数 `filter` 可以用在静态模式规则中来匹配正确的文件。在下面这个例子中,我编造了 `.raw` 和 `.result` 文件扩展名。

```makefile
obj_files = foo.result bar.o lose.o
src_files = foo.raw bar.c lose.c

.PHONY: all
all: $(obj_files)

$(filter %.o,$(obj_files)): %.o: %.c
    echo "target: $@ prereq: $<"
$(filter %.result,$(obj_files)): %.result: %.raw
    echo "target: $@ prereq: $<"

%.c %.raw:
    touch $@

clean:
    rm -f $(src_files)
```

## 模式规则

模式规则虽然常用,但是很令人迷惑。你可以以两种方式来看待它们:

- 一个定义你自己的隐式规则的方式
- 一个静态模式规则的简化形式

先看个例子吧:

```makefile
# Define a pattern rule that compiles every .c file into a .o file
%.o : %.c
      $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@
```

模式规则在目标中包含了一个 `%`,这个 `%` 匹配任意非空字符串,其他字符匹配它们自己。一个模式规则的 `prerequisite` 中的 `%` 表示目标中 `%` 匹配到的同一个词干。


自动变量 `$<` 表示第一个 `prerequisite`。


另一个例子:

```makefile
# Define a pattern rule that has no pattern in the prerequisites.
# This just creates empty .c files when needed.
%.c:
   touch $@
```

## 双冒号规则

双冒号规则虽然很少用到,但是它能为同一个目标定义多个规则。如果换为单冒号的话,系统会输出警告,并且只有第 2 个规则定义的命令会运行。

```makefile
all: blah

blah::
    echo "hello"

blah::
    echo "hello again"
```

# 其他特性

## 包含 Makefiles 文件

`include` 指令告诉 `make` 去读取其他 makefiles 文件,它是 makefile 中的一行,如下所示:

```makefile
include filenames...
```

当你使用像 `-M` 这样的编译器标志时,`include` 特别有用,它可以根据源代码创建 Makefile。例如,如果一些 c 文件包括一个头,这个头将被添加到由 gcc 编写的 Makefile 中。

## `vpath` 指令

`vpath` 指令用来指定某些 `prerequisites` 的位置,使用格式是 `vpath <pattern> <directories, space/colon separated>`。

`<pattern>` 中可以使用 `%`,用来匹配 0 个或多个字符。

你也可以使用变量 `VPATH` 全局执行此操作。

```makefile
vpath %.h ../headers ../other-directory

some_binary: ../headers blah.h
    touch some_binary

../headers:
    mkdir ../headers

blah.h:
    touch ../headers/blah.h

clean:
    rm -rf ../headers
    rm -f some_binary
```

## 多行处理

当命令过长时,反斜杠(`\`)可以让我们使用多行编写形式。

```makefile
some_file:
    echo This line is too long, so \
      it is broken up into multiple lines
```

## `.PHONY`

向一个目标中添加 `.PHONY` 会避免把一个虚拟目标识别为一个文件名。在下面这个例子中,即便文件 `clean` 被创建了,`make clean` 仍会运行。`.PHONY` 非常好用,但是为了简洁起见,在其余示例中我会跳过它。

```makefile
some_file:
    touch some_file
    touch clean

.PHONY: clean
clean:
    rm -f some_file
    rm -f clean
```

## `.DELETE_ON_ERROR`

如果一个命令返回了一个非 0 的退出码,那么 `make` 会停止运行相应的规则(并会传播到它的依赖中)。

如果一个规则因为上述这种情况构建失败了,那么应用了 `.DELETE_ON_ERROR` 后,这个规则的目标文件就会被删除。不像 `.PHONY`,`.DELETE_ON_ERROR` 对所有的目标都有效。始终使用 `.DELETE_ON_ERROR` 是个不错的选择,即使由于历史原因,`make` 不支持它。

```makefile
.DELETE_ON_ERROR:
all: one two

one:
    touch one
    false

two:
    touch two
    false
```

# 目标

## 目标 `all`

想制作多个目标,并且一次运行全部?那就使用 `all` 目标吧。

```makefile
all: one two three

one:
    touch one
two:
    touch two
three:
    touch three

clean:
    rm -f one two three
```



## 多目标

当一个规则(rule)有多个目标时,那么对于每个目标,这个规则下面的 commands 都会运行一次。

`$@` 是一个指代目标名称的 自动变量。

```makefile
all: f1.o f2.o

f1.o f2.o:
    echo $@
# Equivalent to:
# f1.o
#   echo $@
# f2.o
#   echo $@
```

# Makefile Cookbook

让我们来看一个真正生动有趣的例子吧,对于中型规模的项目,它能很好地工作。

这个 makefile 的巧妙之处在于它能自动为你确定依赖,你所要做的就是把你的 C/C++ 文件放到 `src/` 文件夹中。

```makefile
TARGET_EXEC := final_program

BUILD_DIR := ./build
SRC_DIRS := ./src

# Find all the C and C++ files we want to compile
# Note the single quotes around the * expressions. Make will incorrectly expand these otherwise.
SRCS := $(shell find $(SRC_DIRS) -name '*.cpp' -or -name '*.c' -or -name '*.s')

# String substitution for every C/C++ file.
# As an example, hello.cpp turns into ./build/hello.cpp.o
OBJS := $(SRCS:%=$(BUILD_DIR)/%.o)

# String substitution (suffix version without %).
# As an example, ./build/hello.cpp.o turns into ./build/hello.cpp.d
DEPS := $(OBJS:.o=.d)

# Every folder in ./src will need to be passed to GCC so that it can find header files
INC_DIRS := $(shell find $(SRC_DIRS) -type d)
# Add a prefix to INC_DIRS. So moduleA would become -ImoduleA. GCC understands this -I flag
INC_FLAGS := $(addprefix -I,$(INC_DIRS))

# The -MMD and -MP flags together generate Makefiles for us!
# These files will have .d instead of .o as the output.
CPPFLAGS := $(INC_FLAGS) -MMD -MP

# The final build step.
$(BUILD_DIR)/$(TARGET_EXEC): $(OBJS)
    $(CC) $(OBJS) -o $@ $(LDFLAGS)

# Build step for C source
$(BUILD_DIR)/%.c.o: %.c
    mkdir -p $(dir $@)
    $(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

# Build step for C++ source
$(BUILD_DIR)/%.cpp.o: %.cpp
    mkdir -p $(dir $@)
    $(CXX) $(CPPFLAGS) $(CXXFLAGS) -c $< -o $@


.PHONY: clean
clean:
    rm -r $(BUILD_DIR)

# Include the .d makefiles. The - at the front suppresses the errors of missing
# Makefiles. Initially, all the .d files will be missing, and we don't want those
# errors to show up.
-include $(DEPS)
```

断点 发表于 2025-2-8 11:19:22

《一篇文章弄懂Makefile-最简明易懂的Makefile教程》,居然内容是空的?

admin 发表于 2025-2-8 12:40:33

断点 发表于 2025-2-8 11:19
《一篇文章弄懂Makefile-最简明易懂的Makefile教程》,居然内容是空的?

空吗
页: [1]
查看完整版本: 一篇文章弄懂Makefile-最简明易懂的Makefile教程