make 命令详解与参数实测(Ubuntu 22.04)
make 命令逐参数实测:第一个 Makefile 与 @ 前缀、变量五种赋值与自动变量 $@ $< $^、.PHONY 伪目标与 up to date 判定、增量编译与头文件依赖、-n/-t/-q 干跑三件套、命令行变量与 override 优先级、隐式规则与 make -p、@/-/+ 命令前缀与 -i/-k、wildcard/patsubst/shell 函数、ifeq 条件与 include、模式规则与 VPATH、.DEFAULT_GOAL、-C/-f/-j/-B/-s/-w 运行控制与五类报错解读,全部回显在 Ubuntu 22.04(GNU Make 4.3)实测通过。
make 命令详解与参数实测(Ubuntu 22.04)
目标:全面掌握 make 的每一个常用参数——第一个 Makefile 与目标执行、变量五种赋值与自动变量、.PHONY 伪目标、增量编译时间戳判定、
-n/-t/-q干跑三件套、命令行变量覆盖与-e、内置变量与隐式规则、@/-命令前缀与-i/-k、函数 wildcard/patsubst/shell、条件指令与 include、模式规则与 VPATH、.DEFAULT_GOAL、-C/-f/-j/-B/-s/-w运行控制与五类报错,全部命令与回显已在 Ubuntu 22.04(GNU Make 4.3)实测通过。
一、环境信息
- 操作系统:Ubuntu 22.04.5 LTS(x86_64),root 身份执行
- GNU Make 版本:4.3(
make --version实测Built for x86_64-pc-linux-gnu) - 安装来源:make 4.3-4.1build1(
dpkg -S /usr/bin/make归属确认) /usr/bin/gmake -> make为符号链接;make is /usr/bin/make、/bin/make(type -a实测)- 配套工具链:gcc/g++ 11.4.0(本专栏姊妹篇)、cmake 3.22.1(其生成的就是给 make 执行的 Makefile)
- 初始环境:
MAKEFLAGS与MAKELEVEL均为空(顶层调用)
💡 技术拓展:make 是"基于时间戳的构建编排器"——把目标的修改时间与依赖比较,过期才重建。四代构建体系里,make 处于第二层:gcc 负责单次编译(第一层),make 负责多文件增量和依赖编排,cmake(第三层)负责生成 Makefile,CMakePresets/CI(第四层)负责驱动 cmake。理解 make 是看懂 cmake 产物的基础。
二、命令概览
2.1 语法与读取顺序
make 的基本语法是 make [options] [target] ...:不带目标时执行第一个非 . 开头的目标(或 .DEFAULT_GOAL);不带 -f 时依次查找 GNUmakefile、makefile、Makefile(前两个不推荐)。
$ make --version
GNU Make 4.3
Built for x86_64-pc-linux-gnu
Copyright (C) 1988-2020 Free Software Foundation, Inc.
License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html>
This is free software: you are free to change and redistribute it.
There is NO WARRANTY, to the extent permitted by law.
$ make --help | head -30
Usage: make [options] [target] ...
Options:
-b, -m Ignored for compatibility.
-B, --always-make Unconditionally make all targets.
-C DIRECTORY Change to DIRECTORY before doing anything.
-d Print lots of debugging information.
--debug[=FLAGS] Print various types of debugging information.
-e, --environment-overrides Environment variables override makefiles.
-f FILE, --file=FILE, --makefile=FILE
Read FILE as a makefile.
-h, --help Print this message and exit.
-i, --ignore-errors Ignore errors from recipes.
...
$ make --help | wc -l
62
2.2 规则四要素
一条规则的结构:目标: 依赖 + Tab 缩进的命令。时间戳判定、自动变量、命令前缀都围绕它展开。
三、第一个 Makefile
$ cat hello.c
#include <stdio.h>
int main(void)
{
printf("hello, make!\n");
return 0;
}
$ cat Makefile
hello: hello.c
gcc hello.c -o hello
@echo "build done"
$ make
gcc hello.c -o hello
build done
$ ./hello
hello, make!
$ make # 目标已是最新
make: 'hello' is up to date.
$ make hello # 显式指定目标,等价效果
$ touch hello.c && make # 依赖更新 → 重建
gcc hello.c -o hello
build done
命令前缀 @ 静默该行回显(build done 只出了结果没出命令);未加 @ 的 gcc hello.c -o hello 则先打印命令再执行。
文件名大小写皆可:把 Makefile 改名为小写 makefile 后 make 照常工作。找不到任何 makefile 时:
$ make
make: *** No targets specified and no makefile found. Stop.
四、变量:五种赋值与自动变量
$ cat Makefile
A = $(B) # 递归展开:用到时才取 B 的最终值
B = expanded
C := $(D) # 立即展开:此刻 D 还未定义 → 空
D = immediate
LAZY = one
LAZY ?= two # ?= 只在未定义时赋值 → 保持 one
EMPTY =
EMPTY ?= three # EMPTY 已定义为空串(GNU make 4.3 视为已定义)→ 保持空
LAZY ?= four
LIST = a
LIST += b # 追加
show:
@echo "A=$(A) C=$(C)"
@echo "LAZY=$(LAZY) EMPTY=$(EMPTY)"
@echo "LIST=$(LIST)"
prog: main.c util.c
@echo "target=$@ first=$< all=$^"
gcc $^ -o $@
$ make show
A=expanded C=
LAZY=one EMPTY=
LIST=a b
$ make prog
target=prog first=main.c all=main.c util.c
gcc main.c util.c -o prog
$ ./prog && echo exit=$?
exit=0
实测结论:
| 语法 | 名称 | 实测行为 |
|---|---|---|
A = $(B) | 递归展开 | 输出 expanded(延迟到引用时求值) |
C := $(D) | 立即展开 | 输出空(赋值瞬间 D 未定义) |
?= | 条件赋值 | 已定义(含空串)不覆盖 |
+= | 追加 | a b |
$@ $< $^ | 自动变量 | 目标 / 首个依赖 / 全部依赖 |
五、伪目标 .PHONY 与 up to date 判定
$ cat Makefile
all: hello bye
hello:
@echo building hello
touch hello
bye:
@echo building bye
touch bye
clean:
rm -f hello bye
$ make all
building hello
touch hello
building bye
touch bye
$ ls -l hello bye
-rw-r--r-- 1 root root 0 Oct 17 02:52 bye
-rw-r--r-- 1 root root 0 Oct 17 02:52 hello
$ make all
make: Nothing to be done for 'all'. # 文件存在且无依赖 → 什么也不做
问题:若目录里恰好存在名为 clean 的文件,make clean 也会被判定 up to date 而失效。解法是 .PHONY:
$ cat Makefile
.PHONY: all hello clean
all: hello
hello:
@echo "phony hello runs every time"
touch hello
clean:
rm -f hello
$ make hello
phony hello runs every time
touch hello
$ make hello # .PHONY 目标无条件执行
phony hello runs every time
touch hello
$ make clean && ls -l hello
rm -f hello
ls: cannot access 'hello': No such file or directory
$ make nonexistent
make: *** No rule to make target 'nonexistent'. Stop.
六、增量编译工程与头文件依赖
$ cat Makefile
CXX = g++
CXXFLAGS = -std=c++17 -Wall -O2
OBJS = main.o counter.o
app: $(OBJS)
$(CXX) $(OBJS) -o $@
main.o: main.cpp counter.h
counter.o: counter.cpp counter.h
clean:
rm -f $(OBJS) app
$ make
g++ -std=c++17 -Wall -O2 -c -o main.o main.cpp
g++ -std=c++17 -Wall -O2 -c -o counter.o counter.cpp
g++ main.o counter.o -o app
$ ./app
count=2
$ make # 全部最新
make: 'app' is up to date.
$ touch counter.h && make # 头文件变更 → 两个 .o 全部重编
g++ -std=c++17 -Wall -O2 -c -o main.o main.cpp
g++ -std=c++17 -Wall -O2 -c -o counter.o counter.cpp
g++ main.o counter.o -o app
$ make clean && ls
rm -f main.o counter.o app
Makefile counter.cpp counter.h main.cpp
手写头文件依赖容易漏(改了 counter.h 只写了 counter.o 的规则会漏掉 main.o),工程上用 gcc -MMD -MP 自动生成 .d 再 -include 引入。
七、干跑三件套:-n / -t / -q
$ cat app.mk
result.txt: input.txt
cp input.txt result.txt
@echo "rebuilt result.txt"
$ make -f app.mk
cp input.txt result.txt
rebuilt result.txt
$ make -f app.mk
make: 'result.txt' is up to date.
$ sleep 1 && echo "v2" > input.txt && make -f app.mk
cp input.txt result.txt
rebuilt result.txt
$ cat result.txt
v2
$ make -n -f app.mk # 干跑:只打印将执行的命令
make: 'result.txt' is up to date.
$ make -q -f app.mk # 询问模式:最新返回 0
(无输出,退出码 0)
$ make -t -f app.mk # touch 模式:只更新时间戳不执行命令
make: 'result.txt' is up to date.
$ rm -f result.txt && make -n -f app.mk
cp input.txt result.txt
echo "rebuilt result.txt" # 注意 @ 前缀在 -n 下也打印出来
$ make -q -f app.mk # 目标缺失 → 返回 1(真"需要重建")
(无输出,退出码 1)
| 参数 | 语义 | 实测退出码(需重建时) |
|---|---|---|
| -n --just-print | 打印但不执行 | 照常执行打印 |
| -t --touch | 仅 touch 目标 | 0 |
| -q --question | 只问不干 | 1 |
八、命令行变量覆盖与 -e
$ cat Makefile
MODE ?= default
CFLAGS = -Wall
override TOKEN = inner
show:
@echo "MODE=$(MODE)"
@echo "CFLAGS=$(CFLAGS)"
@echo "TOKEN=$(TOKEN)"
export EXPORTED = yes
pass:
@sh -c 'echo "EXPORTED=$$EXPORTED"'
$ make show
MODE=default
CFLAGS=-Wall
TOKEN=inner
$ make show MODE=release # 命令行变量可覆盖 Makefile 赋值
MODE=release
$ make show CFLAGS=-O3
CFLAGS=-O3
$ make -e show CFLAGS=-O3 # -e 让环境变量优先(本例同为命令行传入,效果一致)
CFLAGS=-O3
$ make show TOKEN=outer # override 优先级高于命令行
TOKEN=inner
$ make pass # export 把变量传给子命令环境
EXPORTED=yes
优先级实测:override > 命令行 > Makefile;?= 只兜底。
九、内置变量与隐式规则
$ cat Makefile
CC = gcc
CFLAGS = -Wall -O2
hello: hello.o
$(CC) $(CFLAGS) -o $@ $^
show:
@echo "CC=$(CC) CXX=$(CXX) AR=$(AR) RM=$(RM)"
$ make show
CC=gcc CXX=g++ AR=ar RM=rm -f
$ make hello # 没有 hello.o 的规则,隐式规则自动补上
gcc -Wall -O2 -c -o hello.o hello.c
gcc -Wall -O2 -o hello hello.o
$ ./hello
implicit rule built me
$ rm -f hello hello.o && make hello # 再次验证隐式 .c→.o 链
gcc -Wall -O2 -c -o hello.o hello.c
gcc -Wall -O2 -o hello hello.o
$ make -p -n hello 2>/dev/null | grep -E "^(COMPILE.c|LINK.c|CC =)" | head -4
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
COMPILE.cpp = $(COMPILE.cc)
LINK.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(LDFLAGS) $(TARGET_ARCH)
CC = gcc
make -p(--print-data-base)可导出全部内置规则与变量;.c → .o 由 COMPILE.c、.o → 可执行 由 LINK.c 完成。隐式规则对 .cpp 同样有效(COMPILE.cpp → $(CXX) $(CXXFLAGS))。
十、命令前缀 @ - + 与容错:-i / -k
$ cat Makefile
demo:
@echo "silent line" # @ 静默
-false # - 忽略该行错误
-rm -f /tmp/nonexistent-file-xyz
@date +%F
.PHONY: strict
strict:
false
@echo "never reached"
$ make demo
silent line
2026-10-17 # -false、-rm 的报错都被吞掉
$ make strict
false
make: *** [Makefile:9: strict] Error 1 # 默认出错即停,echo 未执行
$ make -i strict # -i 全局忽略错误
false
make: [Makefile:9: strict] Error 1 (ignored)
never reached # 后续命令继续跑
$ cat Makefile2
.IGNORE:
a:
false
@echo "a survived"
$ make -f Makefile2 a
false
make: [Makefile:3: a] Error 1 (ignored)
a survived # 特殊目标 .IGNORE 等价于每条命令加 -
$ make -k nonexistent1 nonexistent2
make: *** No rule to make target 'nonexistent1'. Stop.
前缀速记:@ 不回显命令、- 出错继续、+ 即使 make 加了 -n/-t/-q 也照常执行(用于必须真实运行的命令)。
十一、常用函数
$ touch a.c b.c c.c
$ cat Makefile
SRCS := $(wildcard *.c)
OBJS := $(patsubst %.c,%.o,$(SRCS))
UPPER := $(subst make,MAKE,i love make)
WORDS := $(words $(SRCS))
MKT := $(shell date +%Y-%m-%d)
EACH := $(foreach f,$(SRCS),[$(f)])
show:
@echo "SRCS=$(SRCS)"
@echo "OBJS=$(OBJS)"
@echo "UPPER=$(UPPER)"
@echo "words=$(WORDS)"
@echo "shell-date=$(MKT)"
@echo "foreach=$(EACH)"
@echo "firstword=$(firstword $(SRCS)) lastword=$(lastword $(SRCS))"
@echo "dir=$(dir src/main.c) notdir=$(notdir src/main.c) basename=$(basename src/main.c)"
$ make show
SRCS=a.c b.c c.c
OBJS=a.o b.o c.o
UPPER=i love MAKE
words=3
shell-date=2026-10-17
foreach=[a.c] [b.c] [c.c]
firstword=a.c lastword=c.c
dir=src/ notdir=main.c basename=src/main
| 函数 | 用途 | 实测 |
|---|---|---|
$(wildcard *.c) | 通配收集 | a.c b.c c.c |
$(patsubst %.c,%.o,…) | 模式替换 | a.o b.o c.o |
$(subst from,to,text) | 字面替换 | i love MAKE |
$(words list) | 计数 | 3 |
$(shell cmd) | 调 shell 取输出 | 2026-10-17 |
$(foreach var,list,text) | 循环拼接 | [a.c] [b.c] [c.c] |
$(dir)/$(notdir)/$(basename) | 路径拆解 | src/ / main.c / src/main |
十二、条件指令与 include
$ cat common.mk
COMMON_VER = 1.0
$ cat Makefile
include common.mk
MODE ?= debug
ifeq ($(MODE),release)
OPT = -O2 -DNDEBUG
else ifeq ($(MODE),profile)
OPT = -O2 -pg
else
OPT = -O0 -g
endif
ifdef EXTRA
OPT += -DEXTRA_ON
endif
show:
@echo "ver=$(COMMON_VER) MODE=$(MODE) OPT=$(OPT)"
-include optional.mk # 前缀 - :文件不存在也不报错
$ make show
ver=1.0 MODE=debug OPT=-O0 -g
$ make show MODE=release
ver=1.0 MODE=release OPT=-O2 -DNDEBUG
$ make show MODE=profile
ver=1.0 MODE=profile OPT=-O2 -pg
$ make show MODE=release EXTRA=1
ver=1.0 MODE=release OPT=-O2 -DNDEBUG -DEXTRA_ON
$ ls optional.mk
ls: cannot access 'optional.mk': No such file or directory
$ make show # -include 缺文件静默通过
ver=1.0 MODE=debug OPT=-O0 -g
条件指令体系:ifeq/ifneq/ifdef/ifndef + else/else ifeq + endif,缩进随意但必须顶格判断。include 缺文件报致命错误,-include(或 sinclude)不报。
十三、模式规则与 VPATH/vpath
$ tree src 2>/dev/null || find src -type f
src/main.cpp src/main.h src/util.cpp src/util.h
$ cat Makefile
CXX = g++
CXXFLAGS = -std=c++17 -Wall
VPATH = src
OBJS = main.o util.o
app: $(OBJS)
$(CXX) $^ -o $@
%.o: %.cpp %.h
$(CXX) $(CXXFLAGS) -c $< -o $@
clean:
rm -f $(OBJS) app
$ make # 源文件在 src/,VPATH 自动找到
g++ -std=c++17 -Wall -c -o main.o src/main.cpp
g++ -std=c++17 -Wall -c -o util.o src/util.cpp
g++ main.o util.o -o app
$ ./app
42
$ make clean && ls
Makefile src
$ cat vpath.mk # vpath 按模式定向搜索
vpath %.cpp src
vpath %.h src
main.o: main.cpp util.h
g++ -c $< -o $@
util.o: util.cpp util.h
g++ -c $< -o $@
app2: main.o util.o
g++ $^ -o $@
$ make -f vpath.mk app2 && ./app2
g++ -c -o main.o src/main.cpp
g++ -c -o util.o src/util.cpp
g++ main.o util.o -o app2
42
VPATH = dir 对一切文件名全局搜索;vpath %.cpp src 按模式限定目录,粒度更细。模式规则 %.o: %.cpp %.h 一条顶 N 条手写规则。
十四、.DEFAULT_GOAL 与 MAKECMDGOALS
$ cat Makefile
.DEFAULT_GOAL := help
help:
@echo "default goal: help"
@echo "requested: $(MAKECMDGOALS)"
build:
@echo "building..."
test: build
@echo "testing..."
clean:
@echo "cleaning..."
$ make # 默认目标被 .DEFAULT_GOAL 改写
default goal: help
requested:
$ make test
building...
testing...
$ make clean build # 多目标按序执行;MAKECMDGOALS 记录命令行目标
cleaning...
building...
$ make --version | head -1
GNU Make 4.3
$ make -f /dev/null
make: *** No targets. Stop.
.DEFAULT_GOAL := help 常用于让 make 裸执行时打印用法;MAKECMDGOALS 只含命令行传入的目标(不含默认注入的),可用它做"make clean 时不要连带构建"之类的防御。
十五、错误场景与解析错误
$ make # 无 makefile
make: *** No targets specified and no makefile found. Stop.
$ cat bad.mk
fail:
exit 3
$ make -f bad.mk fail
exit 3
make: *** [bad.mk:2: fail] Error 3 # 退出码透传
$ cat Makefile
compile:
@echo "step 1 ok"
gcc /nonexistent.c -o out
@echo "step 3 never runs"
$ make compile
step 1 ok
gcc /nonexistent.c -o out
cc1: fatal error: /nonexistent.c: No such file or directory
compilation terminated.
make: *** [Makefile:3: compile] Error 1 # 出错即停
$ make -f bad.mk fail -i
exit 3
make: [bad.mk:2: fail] Error 3 (ignored)
$ make nonexistent-target
make: *** No rule to make target 'nonexistent-target'. Stop.
$ printf 'all:\n echo space-indent\n' > broken.mk # 命令用 4 空格缩进
$ make -f broken.mk
broken.mk:2: *** missing separator. Stop. # 经典错误:必须是 Tab
$ printf 'DEPS = a.c b.c\ntarget: $(DEPS\n\tcat $(DEPS)\n' > broken2.mk
$ make -f broken2.mk
broken2.mk:2: *** unterminated variable reference. Stop. # 括号未闭合
报错速查:
| 报错 | 根因 |
|---|---|
No targets specified and no makefile found | 当前目录没有 makefile 且未给 -f |
No rule to make target 'xxx' | 目标未定义,或依赖文件不存在又无规则生成 |
missing separator | 命令行没用 Tab 缩进(用了空格) |
unterminated variable reference | $(VAR 缺右括号 |
Error N | 规则命令退出码非 0;(ignored) 表示被 -/-i 吞掉 |
十六、运行控制:-C / -f / -j / -B / -s / -w
$ cat Makefile
.DEFAULT_GOAL := all
all:
@echo "built in $(CURDIR)"
$ cat sub/Makefile
all:
@echo "built in $(CURDIR)"
@sleep 0.2
@echo "sub job 1 done"
@sleep 0.2
@echo "sub job 2 done"
$ make -C sub # 先切目录再构建
make: Entering directory '/tmp/mkdemo/run/sub'
built in /tmp/mkdemo/run/sub
sub job 1 done
sub job 2 done
make: Leaving directory '/tmp/mkdemo/run/sub'
$ make -f /tmp/mkdemo/run/Makefile # 绝对路径 makefile
built in /tmp/mkdemo/run
$ make -C sub -w # -w 打印进出目录(-C 自动隐含 -w)
make: Entering directory '/tmp/mkdemo/run/sub'
...
$ make -C sub -s # -s 静默:不回显命令(@ 之外的也不回显)
built in /tmp/mkdemo/run/sub
...
$ make -B -C sub # -B 无条件重建全部目标
make: Entering directory '/tmp/mkdemo/run/sub'
...
$ make -j2 -C sub # -j2 两路并行
make: Entering directory '/tmp/mkdemo/run/sub'
...
$ make -C sub -n # -n 干跑
make: Entering directory '/tmp/mkdemo/run/sub'
echo "built in /tmp/mkdemo/run/sub"
sleep 0.2
...
$ make -C sub -n # -n 下 Entering/Leaving 依旧打印
$ make MAKEFLAGS="-s" -C sub # MAKEFLAGS 环境变量等价传参
built in /tmp/mkdemo/run/sub
...
$ cd /tmp && make -C /tmp/mkdemo/run -s # 任意 cwd 下用 -C 定位
built in /tmp/mkdemo/run
| 参数 | 作用 | 实测要点 |
|---|---|---|
-C <dir> | 进入目录 | 自动附带 Entering/Leaving 提示 |
-f <file> | 指定 makefile | 可为绝对路径 |
-w | 打印目录切换 | -C 时默认开启 |
-s | 静默模式 | 全局 @ |
-B | 无条件重建 | 忽略时间戳 |
-j<N> | 并行作业数 | 多目标/多文件加速;传裸 -j 为不限 |
-n | 干跑打印 | @ 前缀命令也会打印 |
MAKEFLAGS | 环境传参 | 子 make 自动继承 |
十七、参数速查表
| 参数 | 作用 | 实测要点 |
|---|---|---|
-f <file> | 指定 makefile | 缺省按 GNUmakefile/makefile/Makefile |
-C <dir> | 切换目录 | CI 脚本常客 |
-n | 干跑 | 验证规则改动首选 |
-t | 仅 touch | 伪造"最新"状态 |
-q | 询问是否需重建 | 退出码 0/1 |
-B | 强制全部重建 | 时间戳失效时的兜底 |
-j[N] | 并行 | 大工程显著提速 |
-s | 静默 | 等价每条命令加 @ |
-w | 打印目录切换 | -C 隐含 |
-e | 环境变量优先 | 覆盖 Makefile 赋值 |
-i | 忽略命令错误 | 等价全局 - 前缀 |
-k | 出错继续其他目标 | 尽量多收集错误 |
-p | 打印内置数据库 | 查隐式规则 |
VAR=x | 命令行变量 | 优先级仅次于 override |
关联阅读:gcc / g++(规则里真正执行的编译命令)、cmake(生成 Makefile 的上层工具,
cmake --build .内部就是调make)见本专栏姊妹篇。