Makefile学习记录(1):GNU Make入门

成元 2026-04-28 { 教程, 笔记 } [ Makefile, C, C++ ]

前言

之前用了很久 GNU Make,但一直没有仔细整理,这篇文章整理一下相关内容。

这篇会尽量同时满足两种阅读方式:

这一篇先只讲 GNU Make 的基础部分,重点放在:

虽然这一篇先从基础讲起,但我还是会把 条件判断、函数、自动依赖 这些很常见的内容一起整理进来。
不然很多真实项目里的 Makefile,读到一半其实还是会断。

先有一个整体印象

如果先只用一句话概括 GNU Make,我觉得可以这样理解:

Make 不是编译器,它是一个“根据依赖关系决定要不要执行命令”的工具。

比如我们平时在写 C/C++ 项目时,会有这样一串关系:

hello.c -> hello.o -> hello
main.c  -> main.o  -> hello

Makefile 的作用,就是把这种“谁依赖谁、要怎么生成”的关系写出来。

然后每次执行:

make

GNU Make 就会做几件事:

  1. 读取当前目录下的 Makefile
  2. 找到默认目标
  3. 检查目标和依赖文件的修改时间
  4. 只执行那些确实需要重新执行的命令

所以 Make 最核心的一点其实不是“帮你编译”,而是:

它帮你判断哪些东西该重新做,哪些东西不用动。

最小的使用流程

如果是最常见的使用方式,流程通常就是下面这样:

  1. 准备源代码
  2. 编写 Makefile
  3. 在命令行执行 make

例如:

make
make clean

其中:

一条最简单的 Makefile

先看一个最小例子:

hello: hello.c
	gcc hello.c -o hello

它的意思很直接:

这里有一个非常经典的坑:

配方行前面必须是 Tab,不是空格。

很多人第一次写 Makefile 报错,问题就卡在这里。

一条规则到底长什么样

Makefile 最核心的组成单位就是“规则(rule)”。

一条规则通常长这样:

target: prerequisites
	recipe

也就是:

可以粗略理解成:

“如果要得到 target,并且 prerequisites 有变化,那就执行下面这条命令。”

例如:

hello.o: hello.c hello.h
	gcc -c hello.c -o hello.o

它表示:

Make 到底是怎么判断“要不要重新构建”的

初学 Makefile 时,最值得先搞清楚的就是这一点。

对于“目标是文件”的规则,GNU Make 主要看的是文件修改时间

可以先记住这个最基础的判断方式:

例如:

hello: hello.o
	gcc hello.o -o hello

hello.o: hello.c
	gcc -c hello.c -o hello.o

假设现在 hellohello.o 都已经生成好了。

这时如果你只改了 hello.c,再执行:

make

通常会发生的事情是:

  1. make 发现默认目标是 hello
  2. hello 依赖 hello.o,所以先看 hello.o
  3. hello.o 依赖 hello.c,而 hello.chello.o 更新
  4. 所以先重新编译 hello.o
  5. 因为 hello.o 变新了,所以再重新链接 hello

也就是说,Make 本质上是在沿着依赖图往下检查,再按需要往上重建。

为什么通常要把“编译”和“链接”分开

刚开始写时,很多人会先写成这样:

hello: main.c hello.c
	gcc main.c hello.c -o hello

它当然能工作,但有一个问题:
只要 main.chello.c 任何一个改了,整个程序都会重新从头编译并链接。

如果项目很小,这还好。
但文件一多,这种写法就不太合适了。更常见的做法是拆成两层:

hello: main.o hello.o
	gcc main.o hello.o -o hello

main.o: main.c hello.h
	gcc -c main.c -o main.o

hello.o: hello.c hello.h
	gcc -c hello.c -o hello.o

这样做的好处是:

这也是 Make 在 C/C++ 项目里特别常见的原因之一:
它非常适合描述“多个源文件分别编译,最后统一链接”的流程。

默认目标是谁

通常直接执行:

make

GNU Make 会尝试构建 第一条规则的目标

所以很多 Makefile 都会把总目标放在最前面,例如:

all: hello

hello: main.o hello.o
	gcc main.o hello.o -o hello

这时默认目标就是 all

也正因为如此,很多项目会把:

all:

当成一个总入口。

make 默认会找哪个 Makefile

平时我们直接敲:

make

很多时候并没有显式告诉它“你该读哪个文件”。

GNU Make 默认会按这个顺序查找:

  1. GNUmakefile
  2. makefile
  3. Makefile

不过一般还是更推荐用:

Makefile

原因也很简单:

如果你确实想手动指定文件,也可以写:

make -f MyMakefile
make --file=MyMakefile

这一点在切换不同构建脚本、或者做实验性 Makefile 时挺有用。

先看一个稍微完整一点的例子

下面这个例子比前面稍微像样一点:

app: main.o calc.o
	gcc main.o calc.o -o app

main.o: main.c calc.h
	gcc -c main.c -o main.o

calc.o: calc.c calc.h
	gcc -c calc.c -o calc.o

clean:
	rm -f app main.o calc.o

如果只看结构,这里已经出现了 Makefile 里最常见的几种目标:

这个版本已经能正常工作,但还不够“像 GNU Make”。
因为里面还有不少重复内容可以继续提炼。

用变量减少重复

写 Makefile 时,一个很常见的问题就是重复。

例如下面这条规则里,目标文件列表写了两遍:

app: main.o calc.o
	gcc main.o calc.o -o app

文件少的时候还好,文件一多就很容易漏改。

所以通常会先提一个变量出来:

OBJS = main.o calc.o

app: $(OBJS)
	gcc $(OBJS) -o app

main.o: main.c calc.h
	gcc -c main.c -o main.o

calc.o: calc.c calc.h
	gcc -c calc.c -o calc.o

clean:
	rm -f app $(OBJS)

这里的 $(OBJS) 就是变量引用。

如果你只记一种写法,记这个就够了:

NAME = value
$(NAME)

=:= 可以先粗略怎么区分

GNU Make 的变量展开细节其实不少,但入门时我觉得先抓最常见的区别就够用了。

最常见的两种赋值方式是:

VAR = value
VAR2 := value

可以先粗略理解成:

看一个很小的例子:

A = $(B)
B = hello

C := $(D)
D := world

那么:

所以如果只是日常写简单 Makefile:

?=:给变量一个“默认值”

除了 =:=,还有一种在工程 Makefile 里很常见的写法:

PREFIX ?= /usr/local

它可以粗略理解成:

如果 PREFIX 还没有被设置,那就给它一个默认值。

这类写法常见在:

例如:

CC ?= gcc
PREFIX ?= /usr/local

这样 Makefile 自己有一套默认行为,但使用者又可以按需改掉。

命令行也可以直接覆盖变量

GNU Make 一个很实用的地方是:
你可以在命令行里直接给变量赋值。

例如:

make CC=clang
make CFLAGS='-O0 -g'
make PREFIX=/tmp install

这在日常里非常方便,因为它能让 Makefile 保持通用,同时又不必为了某次临时构建去修改文件本身。

如果和前面的 ?= 放在一起看,会更容易理解它的用途:

所以像:

PREFIX ?= /usr/local

就很适合配合:

make PREFIX=/opt/app install

一起使用。

GNU Make 内置了很多隐式规则

前面我们是这样写的:

main.o: main.c calc.h
	gcc -c main.c -o main.o

calc.o: calc.c calc.h
	gcc -c calc.c -o calc.o

但对于 .c -> .o 这种太常见的事情,GNU Make 本来就知道怎么做。

也就是说,你很多时候其实可以只写依赖,不必把编译命令重复写一遍:

OBJS = main.o calc.o

app: $(OBJS)
	gcc $(OBJS) -o app

main.o: calc.h
calc.o: calc.h

这里之所以能工作,是因为 GNU Make 有内置的隐式规则,会尝试推断:

main.o <- main.c
calc.o <- calc.c

如果项目规则比较标准,这种写法会简洁很多。

不过也正因为是“自动推断”,所以当行为不符合预期时,排查起来会稍微绕一点。
初学阶段如果觉得不安心,先把命令显式写出来也完全可以。

常见的内置变量

GNU Make 除了内置规则,还约定了一些常用变量名。

在 C/C++ 项目里最常见的有:

CC = gcc
CXX = g++
CFLAGS = -O2 -Wall
CXXFLAGS = -O2 -Wall
CPPFLAGS = -Iinclude
LDFLAGS =
LDLIBS =

可以先粗略记成:

例如:

CC = gcc
CPPFLAGS = -Iinclude
CFLAGS = -O2 -Wall
LDLIBS = -lm

app: main.o calc.o
	$(CC) main.o calc.o -o app $(LDLIBS)

这样后面如果换编译器、加优化选项、改头文件路径,改起来会轻松很多。

自动变量:把“当前这条规则里的东西”拿出来用

写 Makefile 时,自动变量特别常用。

最常见的几个是:

先看最常见的两个例子:

hello: hello.o main.o
	gcc $^ -o $@

hello.o: hello.c
	gcc -c $< -o $@

它们展开后大概就是:

所以自动变量最大的意义其实就是:
少重复写文件名,让规则更通用。

用模式规则一次处理一批 .c -> .o

如果每个 .o 文件都手写一遍,还是会有点烦。
这时通常会进一步写成模式规则:

CC = gcc
CPPFLAGS = -Iinclude
CFLAGS = -O2 -Wall
OBJS = main.o calc.o

app: $(OBJS)
	$(CC) $^ -o $@

%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

clean:
	rm -f app $(OBJS)

这里:

例如要生成 main.o 时:

这类规则在 GNU Make 里非常重要。
因为很多时候你真正想表达的并不是“这个文件怎么编”,而是“这类文件都按这个办法编”。

一个更像日常项目的入门版 Makefile

如果把前面的东西揉到一起,一个比较常见、也比较适合入门项目起步的版本,大概会长这样:

CC = gcc
CPPFLAGS = -Iinclude
CFLAGS = -O2 -Wall
LDLIBS = -lm

TARGET = app
SRCS = main.c calc.c
OBJS = $(SRCS:.c=.o)

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $^ -o $@ $(LDLIBS)

%.o: %.c calc.h
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(TARGET) $(OBJS)

这里又出现了一个常见写法:

OBJS = $(SRCS:.c=.o)

它的意思是把 SRCS 里的每个 .c 替换成 .o

如果:

SRCS = main.c calc.c

那么:

OBJS = main.o calc.o

这也是 GNU Make 很常见的一种“变量替换引用”。

.PHONY 是什么,为什么 clean 常常要配它

cleanall 这种目标,很多时候并不真的对应某个文件。
它们更像“动作名称”。

这类目标通常会写成伪目标:

.PHONY: clean

clean:
	rm -f app *.o

为什么需要 .PHONY

因为如果当前目录下刚好真的有一个文件叫 clean,那么 Make 可能会以为这个目标已经是“最新的”,从而不执行对应命令。

加上:

.PHONY: clean

之后,GNU Make 就知道:
clean 不是一个真实文件,而是一个始终可以执行的操作目标。

通常常见的伪目标有:

make clean 为什么不会默认执行

这一点初学时也挺容易混。

像下面这种规则:

clean:
	rm -f app *.o

并不会在你直接执行 make 时自动跑。

因为 make 默认只会从默认目标开始,沿着它的依赖关系往下做事。

clean 通常既不是第一条规则,也不是其他目标的依赖,所以它不会自动执行。

只有你明确写:

make clean

它才会去执行 clean 这个目标。

依赖不一定都是“重建条件”

GNU Make 里有一个初学阶段不一定马上用到,但知道了很有帮助的概念:
order-only prerequisites(仅顺序依赖)

普通依赖有两层含义:

  1. 先把依赖处理好
  2. 如果依赖比目标新,就重建目标

但有时候我们只想保留第一层,不想要第二层。

最典型的例子就是“输出目录必须先存在,但目录时间戳变化不应该导致所有目标重编”。

例如:

OBJDIR = build
OBJS = build/main.o build/calc.o

app: $(OBJS)
	gcc $^ -o $@

build/%.o: %.c | $(OBJDIR)
	gcc -c $< -o $@

$(OBJDIR):
	mkdir -p $(OBJDIR)

这里:

| $(OBJDIR)

表示 $(OBJDIR) 是一个仅顺序依赖

这个写法在稍微正规一点的工程里很常见。

通配符和 wildcard

Makefile 里经常会看到 *,但它不是在任何地方都一样好使。

例如在规则里:

clean:
	rm -f *.o

这里的 *.o 是 shell 在执行命令时展开的。

但如果你在变量定义里直接写:

SRCS = *.c

那它通常不会自动变成当前目录下所有 .c 文件。

这时更常见的写法是:

SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)

其中:

如果项目文件数量固定,我一般更倾向于手写文件列表。
如果项目文件会经常增删,再考虑 wildcard 会更省事一点。

几个很实用的 make 命令行选项

前面主要都在讲 Makefile 怎么写。
但实际使用 GNU Make 时,命令行参数也很重要,尤其是在排错和确认行为的时候。

我觉得入门阶段最值得先记住的是下面这些:

make -n:只看会执行什么,不真正执行

make -n
make -n clean

这个参数也叫 --just-print--dry-run

它的作用是:

这在两种场景里特别有用:

如果把它和前面讲的依赖关系一起看,其实 make -n 很适合拿来观察:

make -B:强制全部重建

make -B

这个参数是 --always-make

它的意思可以粗略理解成:

别看时间戳了,相关目标都按需要重做一遍。

它通常适合:

当然,这样做的代价也很直接:会慢一些。

make -dmake --trace:排查 Make 到底在想什么

如果你写 Makefile 时遇到这种问题:

那就可以看看:

make -d
make --trace

其中:

我自己会更建议先试 --trace,因为 -d 的输出通常非常多。
如果 --trace 还不够,再上 -d 会更容易看。

make -j:并行执行

make -j4

这个参数表示允许同时跑多个任务。

对于多文件编译项目来说,如果依赖关系写得合理,-j 往往能明显加快构建速度。

不过这里也有一个前提:

Makefile 里的依赖关系必须写对。

因为并行构建会把一些原来“串行时碰巧没出问题”的隐藏依赖暴露出来。

所以刚开始学习时,我更建议先把普通串行构建逻辑写顺,再去加:

make -j

make -C:先切到别的目录再构建

make -C src

这个参数的意思是:

如果你在上层目录想调用子目录里的构建逻辑,这个参数会很方便。

make -p:把 Make 当前看到的规则和变量打印出来

make -p

这个参数会打印 Make 读完 Makefile 之后形成的数据库,包括:

它的输出会很多,但在你怀疑:

的时候会很有帮助。

make --warn-undefined-variables

make --warn-undefined-variables

这个参数会在引用未定义变量时给出警告。

它特别适合抓这类问题:

Makefile 里变量很多的时候,这类错误其实挺隐蔽的,所以这个选项很好用。

include:把别的 Makefile 片段读进来

GNU Make 支持:

include other.mk

它的作用很像“把另一个文件内容插进来继续读”。

这个功能很常见的用途有:

不过作为入门阶段,我觉得先知道它的存在就够了。
如果一开始就把 Makefile 拆太散,反而不利于理解主线。

从“单文件示例”走向“项目骨架”

前面那些例子主要是为了讲清楚 Make 的基本机制。
但一旦开始把它用于实际项目,通常就不只是“一个可执行文件 + 几个 .c 文件”这么简单了。

比较常见的目录组织方式大概像这样:

project/
├── Makefile
├── src/
├── include/
├── tests/
├── build/
└── bin/

可以先粗略理解成:

这不是什么硬性标准,但它反映了一种很常见的思路:

Makefile 不只是“编一两个文件”,而是在组织一个项目的构建流程。

所以当项目慢慢变大时,Makefile 里通常也会开始出现:

也就是说,Makefile 既可以很小,也可以逐渐演变成一个项目级的构建入口。

@:只显示命令结果,不显示命令本身

前面所有例子里,命令行默认都会被 Make 打印出来。

有时候这正是我们想要的,因为方便观察构建过程。
但有些规则里,我们只想显示结果,不想把命令本身也打印出来。

这时可以在命令前面加 @

check:
	@echo start check

这样执行 make check 时,通常只会看到:

start check

而不会先把:

echo start check

打印出来。

这个写法很常见于:

$(MAKE):在 Makefile 里再调用 make

如果一个目标里需要继续调用另一个 make,通常更推荐写:

$(MAKE)

而不是直接手写:

make

例如:

subsystem:
	$(MAKE) -C src

这样做的好处主要是:

这个点在递归构建、子目录构建里会比较常见。

define:定义多行变量或预制配方

到这里为止,我们前面写的变量基本都是单行。
但 GNU Make 其实也很常见把一段多行内容先存进变量里,再在后面展开使用。

最常见的写法是:

define RUN_CC
$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@
endef

然后在规则里用:

%.o: %.c
	$(RUN_CC)

也可以把它理解成:

把一段多行文本先打包成变量,后面按需要展开。

这个能力常见在两种场景里:

不过这里要注意一点:
define 本质上还是变量,不是“新规则语法”。

也就是说,它只是先把文本存起来。
如果你想让展开后的文本再被 Make 当成真正的 Makefile 语句重新解析,通常还要配合后面会提到的 eval

条件判断:按场景切换构建逻辑

GNU Make 的条件判断最常见的几种写法是:

ifeq ($(CC),clang)
    CFLAGS += -Weverything
endif

ifneq ($(DEBUG),1)
    CFLAGS += -O2
endif

ifdef CROSS_COMPILE
    CC := $(CROSS_COMPILE)gcc
endif

ifndef BUILD_DIR
    BUILD_DIR := build
endif

可以先粗略记成:

这类判断非常适合做:

例如一个很常见的写法:

DEBUG ?= 0

ifeq ($(DEBUG),1)
    CFLAGS += -O0 -g
else
    CFLAGS += -O2
endif

这样就可以通过:

make DEBUG=1

切到调试构建。

这里还有一个很重要的细节:
Make 的条件判断是在读取 Makefile 时就处理的。

这意味着:

如果你的判断依赖于规则真正执行时的上下文,那通常就该把判断写进 shell 命令,而不是写成 ifeq

常见函数:Makefile 开始“像编程”的地方

GNU Make 的函数很多,但我觉得最值得先掌握、也最常在项目里看到的,主要是下面这些:

patsubst

前面我们已经见过:

OBJS = $(SRCS:.c=.o)

这个本质上就是一种替换。
写成函数形式就是:

OBJS = $(patsubst %.c,%.o,$(SRCS))

如果只是简单地改后缀,前面的简写通常就够用了。
但如果模式稍微复杂一点,patsubst 会更清楚。

filter

这个函数适合从一堆文件名里筛出自己想要的那一类。

例如:

FILES = main.c calc.c readme.md
CSRCS = $(filter %.c,$(FILES))

那么:

CSRCS = main.c calc.c

addprefixaddsuffix

它们很适合给一组名字统一补目录或后缀。

例如:

NAMES = main calc
SRCS = $(addsuffix .c,$(NAMES))
OBJS = $(addprefix build/,$(NAMES))
OBJS := $(addsuffix .o,$(OBJS))

这样就能很自然地拼出 build/main.o build/calc.o 这种列表。

foreach

这个函数适合把一个列表映射成另一种形式。

例如:

DIRS = src lib tests
INCFLAGS = $(foreach dir,$(DIRS),-I$(dir))

结果大概就是:

-Isrc -Ilib -Itests

call

如果你开始觉得某些表达式一直在重复,通常就会遇到 call

例如:

to_obj = $(patsubst %.c,%.o,$(1))
OBJS = $(call to_obj,$(SRCS))

这里可以先粗略理解成:

它不算第一天就必须会写,但很值得能看懂。

shell

这个函数表示执行一个外部命令,并把输出结果拿回来作为变量值。

例如:

CUR_GIT_HASH := $(shell git rev-parse --short HEAD)

它很方便,但最好别滥用,因为:

我的倾向是:确实需要从外部拿信息时再用。

eval

这是 GNU Make 比较“元编程”的一个能力。

它的作用可以先粗略理解成:

把展开后的文本,再当成 Makefile 语句重新解析一次。

例如:

define PROGRAM_template
$(1): $$($(1)_OBJS)
	$$(CC) $$^ -o $$@
endef

$(eval $(call PROGRAM_template,app))

如果没有 eval,很多多行模板最后只会变成普通字符串;
有了 eval,它们才会真的生成规则。

自动生成头文件依赖

前面我们一直在手写:

main.o: main.c calc.h
calc.o: calc.c calc.h

但真实项目里,头文件一多,这种写法很容易漏。

GNU Make 文档里很推荐的一种思路是:

如果用 GCC 或 Clang,日常里更常见的简写通常会是:

CPPFLAGS += -MMD -MP

SRCS = main.c calc.c
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)

-include $(DEPS)

然后编译规则还是:

%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

这样在编译 .o 的同时,编译器也会顺手生成对应的 .d 文件。

我觉得这部分非常值得尽早掌握,因为它解决的是一个很常见的问题:

头文件明明改了,但 Makefile 里没写到依赖,结果目标没有重编。

一个带条件和自动依赖的稍微完整版本

如果把前面的几块揉在一起,一个更接近真实项目起步状态的版本大概会长这样:

CC ?= gcc
DEBUG ?= 0

CPPFLAGS += -Iinclude -MMD -MP
CFLAGS += -Wall
LDLIBS += -lm

ifeq ($(DEBUG),1)
    CFLAGS += -O0 -g
else
    CFLAGS += -O2
endif

TARGET = app
SRCS = $(wildcard *.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)

.PHONY: all clean

all: $(TARGET)

$(TARGET): $(OBJS)
	$(CC) $^ -o $@ $(LDLIBS)

%.o: %.c
	$(CC) $(CPPFLAGS) $(CFLAGS) -c $< -o $@

clean:
	rm -f $(TARGET) $(OBJS) $(DEPS)

-include $(DEPS)

这个版本已经比“纯手写依赖”的版本更像一个可以往后继续扩的工程 Makefile 了。

.SECONDEXPANSION:先知道它在干什么

这个功能不算入门第一优先级,但你后面读一些比较灵活的 Makefile 时,可能会碰到。

它的典型用途是:

例如:

.SECONDEXPANSION:
app_OBJS := main.o calc.o
test_OBJS := test.o calc.o

app test: $$($$@_OBJS)

这里可以粗略理解成:

也就是说,它让“不同目标自动拿各自那套变量”这件事变得更自然。

这个功能很强,但我觉得第一次学 Make 没必要一开始就上手写。
先做到“看到时知道它在干什么”,已经很够用了。

一个从零开始比较顺手的学习顺序

如果你是第一次比较系统地看 GNU Make,我觉得下面这个顺序会比较顺:

  1. 先理解“目标、依赖、命令”三件事
  2. 再理解 Make 是靠文件时间戳判断要不要更新
  3. 然后理解“编译”和“链接”为什么要分开
  4. 接着补变量和自动变量
  5. 然后看隐式规则、模式规则、伪目标
  6. 最后再补条件判断、函数、自动依赖

也就是说,Makefile 最难的地方通常不是语法本身,而是:
你得先把“构建过程”这件事拆成依赖图来看。

快速手册

如果只是临时想回忆常见写法,可以先看这一节。

最基本的规则

target: prerequisites
	command

常用命令

make
make <target>
make clean
make -f MyMakefile
make -n
make -B
make -j4
make -C dir
make --trace
make --warn-undefined-variables

常见变量写法

CC = gcc
CXX = g++
CFLAGS = -O2 -Wall
CPPFLAGS = -Iinclude
LDFLAGS =
LDLIBS = -lm
PREFIX ?= /usr/local

条件判断

ifeq ($(DEBUG),1)
    CFLAGS += -O0 -g
else
    CFLAGS += -O2
endif

常见自动变量

$@   # 当前目标
$<   # 第一个先决条件
$^   # 所有先决条件

最常见的模式规则

%.o: %.c
	$(CC) -c $< -o $@

伪目标

.PHONY: all clean

变量替换

SRCS = main.c calc.c
OBJS = $(SRCS:.c=.o)

通配符函数

SRCS := $(wildcard *.c)

常见函数

OBJS := $(patsubst %.c,%.o,$(SRCS))
CSRCS := $(filter %.c,$(FILES))
INCFLAGS := $(foreach dir,$(DIRS),-I$(dir))

命令行覆盖变量

make CC=clang
make CFLAGS='-O0 -g'
make PREFIX=/tmp install

安静执行一条命令

	@echo hello

仅顺序依赖

target: normal-deps | order-only-deps

自动依赖

CPPFLAGS += -MMD -MP
DEPS := $(OBJS:.o=.d)
-include $(DEPS)

总结

GNU Make 入门时,我觉得最容易让人混乱的地方,不是变量多,也不是符号多,而是很多人一开始会把它当成“编译命令收集器”。

但更准确一点地说,Make 真正在做的事情其实是:描述依赖关系,然后根据文件状态决定哪些命令要执行。

只要把这件事想清楚,后面的很多写法就会顺不少。

比如:

如果只是日常写小项目的 Makefile,其实先掌握这一篇里的内容,大多数基础场景已经够用了。
后面再去读别人项目时,我觉得可以重点看这几件事:

参考资料