Makefile学习笔记

5.9k words

一个基本的Makefile

1
2
hello:
echo "hello world!"

使用make执行这个Makefile 结果是

1
2
echo "hello world!"
hello world!

创建一个hello文件后再make结果是

1
make: 'hello' is up to date.

这是 make 的经典行为:

只要目标文件(hello)已经存在,并且所有依赖为最新,make 就不会重复执行。

但现在 hello 目标没有任何依赖,所以:

  • make 只看有没有 hello 这个文件
  • 如果已经存在这个文件,并且没有任何依赖需要更新它
  • 就会判断它是 up to date(已经是最新的)

Makefile语法

一个Makefile由一系列rule组成,一个rule的基本结构为:

1
2
3
4
targets: prerequisites
command
command
command
  • 目标(targets)是文件名,使用空格分隔。通常每条规则只包含一个目标。
  • 命令(commands)是一系列步骤,通常用于生成目标文件。每条命令前必须以一个制表符(Tab 字符)开头,而不是空格。
  • 前置条件(prerequisites)也是文件名,使用空格分隔。在执行生成目标的命令之前,这些文件必须已经存在。前置条件也被称为依赖(dependencies)。

Make的本质

创建一个新的Makefile

1
2
blah: 
cc blah.c -o blah

在第一次make时结果为

1
cc blah.c -o blah

第二次make的结果为

1
make: 'blah' is up to date.

然而在我们修改blah.c文件后再make时,结果依然是第二次make时的结果。似乎与我们想要的结果不一样。

要解决这个问题,我们只需要将Makefile修改为

1
2
blah: blah.c
cc blah.c -o blah

这样,当blah.c发生改变时,blah也会重新生成。

当我们再次运行 make 时,会发生以下一系列步骤:

  1. 首先会选择第一个目标,因为第一个目标是默认目标;

  2. 这个目标有一个先决条件(prerequisite),比如 blah.c

  3. make 接下来会判断是否需要执行 blah 目标。它只会在以下两种情况下执行:

    • blah 这个文件不存在;

    • 或者 blah.c 的修改时间比 blah 更新。

最后这一步非常关键,它是 make 的核心。make 试图判断自从上次编译 blah 以来,blah 的先决条件(这里是 blah.c)是否发生了变化。也就是说,如果你修改了 blah.c,那么运行 make 时应该重新编译这个文件。反过来,如果 blah.c 没有变,就不应该重新编译。

为了实现这一点,make 利用文件系统的时间戳作为是否变更的判断依据。这个方法在大多数情况下是合理的,因为文件的时间戳通常只会在文件被修改时才更新。

但重要的是你要明白:这个方法并不总是准确的。比如说,你可以手动修改一个文件,然后把它的修改时间改成一个更早的时间。如果你这么做了,make错误地认为这个文件没有变化,于是就不会重新编译。

Make clean

clean 通常用作一个目标,用于删除其他目标的输出,但它在 Make 中并不是一个特殊的词。你可以通过运行 makemake clean 来创建和删除 some_file

需要注意的是,clean 在这里做了两件新事情:

  1. 它是一个不是默认的第一个目标,也不是一个先决条件的目标。这意味着除非你显式地运行 make clean,否则它不会执行。
  2. 它并不是一个文件名。如果恰好存在一个名为 clean 的文件,那么这个目标就不会执行,这是我们不希望的。

变量

变量只能是字符串,值通常为文件名。有 := 和 = 赋值。

=(延迟展开/递归赋值)

1
2
FOO = $(BAR)
BAR = hello
  • FOO 被使用时,才会去解析 $(BAR)
  • 所以 FOO 的值是 hello
  • 如果你在后面修改了 BARFOO 的值也会随之改变。

✔适用于:需要在使用时动态计算值的场景。


:=(立即展开/简单赋值)

1
2
FOO := $(BAR)
BAR = hello
  • 当这一行被解析时,$(BAR) 立刻被展开(即“立即求值”)。
  • 此时 BAR 还没有被赋值,所以 FOO 是空的。

✔适用于:只想计算一次,或者确保变量在赋值时就固定不变的情况。

建议使用和${}$()来引用变量。

targets

当规则有多个目标时,将针对每个目标运行命令。$@是一个包含目标名称的自动变量。

1
2
3
4
5
6
7
8
9
all: f1.o f2.o

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

通配符

在 Makefile 中,* 是一种通配符(wildcard),它和 shell 中的意思类似,用于匹配任意长度的字符串,但它的行为取决于使用的上下文。下面来具体讲讲它的几种用法:


✅ 1. 在规则的依赖或目标中使用 *

这时候的 * 是由 Make 自动解释 的,可以与模式规则(Pattern Rule)一起使用:

例子:

1
2
3
# 规则:把所有 .c 文件编译成 .o 文件
%.o: %.c
gcc -c $< -o $@
  • %.o%.c 是模式匹配,% 相当于一个通用“变量”,匹配文件名中的任意部分。
  • 例如:当 main.c 存在时,%.c -> main.c,就会自动生成对应的 main.o

✅ 2. 在变量中使用 * 通配符(通常结合 wildcard 函数)

此时 * 不会自动展开,需要用 $(wildcard ...) 函数显式展开

1
SRC := $(wildcard src/*.c)
  • src/*.c 中的 * 匹配 src/ 目录下所有以 .c 结尾的文件。

✅ 3. 在命令中使用 *(shell通配符)

如果你在命令里直接写 *,Make 会调用 shell 解释它,就像你在终端里输入的一样。

1
2
clean:
rm -f *.o
  • 这里的 * 是由 shell 解释的,删除所有 .o 文件。

🔍 总结:Makefile 中的 * 用法一览

场景 是否需要函数 作用
模式规则(如 %.o: %.c 匹配任意文件名片段
变量中使用(如 SRC := *.c ✅ 需要 wildcard 获取实际匹配的文件名
shell 命令中(如 rm *.o 由 shell 展开

以下是一个例子:

1
2
3
打印所有的.c文件
print: $(wildcard *.c)
ls -la $?

这是个很关键的问题,Makefile 中通配符未匹配到任何文件时的行为确实值得细讲。下面我分几种情况来给你讲清楚:


🔍 1. 直接写通配符(未展开)时:

示例:

1
target: *.o

如果当前目录下没有任何 .o 文件,**\*.o 不会被展开为空,而是保持字面字符串 "\*.o"**。

❗结果:

  • Make 会把 "*.o" 当成一个 真实的文件名(叫 *.o)。
  • 如果没有叫 *.o 的文件存在,Make 会认为这个依赖是 缺失的,然后:
    • 报错:No rule to make target '*.o', needed by 'target'.
    • 或者:尝试按默认规则找如何生成 *.o,但几乎不会成功。

🔍 2. 使用 $(wildcard \*.o) 时:

示例:

1
2
FILES := $(wildcard *.o)
target: $(FILES)

如果没有 .o 文件,$(wildcard *.o) 会被展开为 空字符串

✅结果:

  • 依赖变成 target:,即没有依赖
  • Make 会继续执行 target 的命令(如果你有写命令)
  • 不会报错,也不会尝试构建 *.o 这个文件

✅ 推荐写法:始终用 $(wildcard ...) 包裹通配符

1
2
3
4
OBJS := $(wildcard *.o)

target: $(OBJS)
@echo "building with: $(OBJS)"

这样即使没有 .o 文件,也不会出错,OBJS 就是空,target 的命令仍会正常执行(取决于是否需要依赖)。


⚠️ 顺便提醒一个坑:用 wildcard 后别滥用 ?

如果你写了:

1
2
target: $(wildcard *.o)
@echo $? # 这是空的,因为没有依赖比目标新

此时 $? 是空的,不会报错,但什么都不会输出,也不会编译任何文件。


🔚 总结

情况 没有匹配时的行为
*.o 直接用 被当成一个叫 *.o 的文件,找不到就报错
$(wildcard *.o) 展开为空字符串,依赖为空,不会报错

你这段关于 % 的描述相当准确!它说出了 % 在 Makefile 中的两种核心用法:匹配(matching)替换(replacing)。下面我来帮你具体拆解一下这两个模式的用法和差异,并配合实际例子说明 % 是怎么工作的。


🧠 % 的本质是什么?

在 Make 中,% 是一个通配符,但它不是文件系统通配符,而是 Makefile 专属的模式匹配符号,用于规则或函数中。


✅ 用法一:匹配模式(Matching) — 找出“stem”

📌 使用场景:规则定义中的目标依赖关系

1
2
%.o: %.c
gcc -c $< -o $@

🧩 含义:

  • % 匹配任意字符(至少一个),称为 stem(词干)
  • 比如 main.omain.cstem = main
  • Make 会自动将 %.o%.c 配对,构建规则

✅ 自动变量回顾:

变量 含义
$@ 目标,比如 main.o
$< 第一个依赖,比如 main.c
$* stem,即 main

✅ 用法二:替换模式(Replacing) — 使用 stem 替换路径

📌 使用场景:函数中进行字符串变换

1
2
SRCS := main.c foo.c bar.c
OBJS := $(SRCS:%.c=%.o)

🧩 含义:

  • %.c=%.o 表示把所有 .c 文件的 stem 替换成 .o
  • main.cmain.o
  • foo.cfoo.o

这是 %:=$(...) 变量表达式中非常强大的替换用法。


💡 进阶:模式规则和替换配合使用

你可以组合使用 % 来实现自动编译流程:

1
2
3
4
5
6
7
SRCS := $(wildcard *.c)
OBJS := $(SRCS:%.c=%.o)

all: $(OBJS)

%.o: %.c
gcc -c $< -o $@
  • 利用 wildcard 获取 .c 文件
  • 利用 %.c=%.o 替换生成 .o 文件列表
  • 利用 %.o: %.c 写模式规则自动构建

这个模板非常常用,在实际项目中非常好使。


✅ 总结表格:% 的两种用法对比

用法 场景 示例 说明
匹配 规则模式匹配 %.o: %.c main.c 编成 main.ostem = main
替换 字符串变换 $(SRCS:%.c=%.o) .c 文件换成 .o

自动变量

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
hey: one two
# Outputs "hey", since this is the target name
echo $@

# Outputs all prerequisites newer than the target
echo $?

# Outputs all prerequisites
echo $^

# Outputs the first prerequisite
echo $<

touch hey

one:
touch one

two:
touch two

clean:
rm -f hey one two
1
2
3
4
5
target: dep1 dep2
echo $@ # => target
echo $? # => 所有比 target 新的依赖(时间戳比较)
echo $^ # => 所有依赖(去重)
echo $< # => 第一个依赖

有趣的规则

隐式规则

1
2
3
4
5
6
7
8
9
10
11
12
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*
变量 含义 示例值(可设定)
CC 编译 C 的命令 默认是 cc,常设为 gcc
CXX 编译 C++ 的命令 默认是 g++
CFLAGS 给 C 编译器的额外参数 -Wall -O2
CXXFLAGS 给 C++ 编译器的额外参数 -std=c++17 -O2
CPPFLAGS 给预处理器的参数 -Iinclude -DMY_DEFINE
LDFLAGS 链接器参数 -Llib/
LDLIBS 链接时要用到的库 -lm -lpthread
目标文件 来源 默认命令(隐式规则)
n.o n.c $(CC) -c $(CPPFLAGS) $(CFLAGS) $^ -o $@
n.o n.cpp / n.cc $(CXX) -c $(CPPFLAGS) $(CXXFLAGS) $^ -o $@
n(可执行文件) n.o $(CC) $(LDFLAGS) $^ $(LOADLIBES) $(LDLIBS) -o $@

静态模式规则

📜 静态模式规则的语法

1
2
targets...: target-pattern: prereq-patterns ...
commands

语法解释:

  • **targets…**:多个目标文件
  • target-pattern:目标的模式(通常是文件名模式)
  • prereq-patterns:依赖的模式(也可以是文件名模式)
  • commands:执行的命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
objects = foo.o bar.o all.o
all: $(objects)
$(CC) $^ -o all

# 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
$(CC) -c $^ -o $@

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

# Note: all.c does not use this rule because Make prioritizes more specific matches when there is more than one match.
%.c:
touch $@

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