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)实测通过。

Linux命令makeLinux系统UbuntuUbuntu 22.04命令行2026/10/17

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)见本专栏姊妹篇。