make最佳实践手册

Copy
# 伪目标
PHONY := __build
__build: #空操作

# 清空需要的变量
obj-y := #需要编译的目标文件列表
subdir-y := #子目录列表
EXTRA_CFLAGS := #额外编译选项

# 包含同级目录Makefile
include Makefile

# 获取当前 Makefile 需要编译的子目录的目录名
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y)) : c/ d/
# __subdir-y : c d
# subdir-y : c d
__subdir-y := $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y += $(__subdir-y)

# 生成各子目录的目标文件列表：要编译的子目录下目标文件都打包成了dir/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# 过滤 obj-y，得到当前目录下需要编进程序的文件名作为，并写为目标
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 使修改头文件 .h 后，重新make后可以重新编译（重要）
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 包含所有依赖文件
dep_files := $(wildcard $(dep_files))
ifneq ($(dep_files),) #不为空（即存在依赖文件），则执行下一步操
include $(dep_files) #Make 工具会读取这些依赖文件，从而确保 Makefile 知道源文件与头文件之间的正确依赖关系，当头文件修改后可以重新编译受影响的源文件
endif

PHONY += $(subdir-y)
# 第一个目标
__build : $(subdir-y) built-in.o
# 优先编译 子目录的内容;-C切换到子目录执行$(TOPDIR)/Makefile.build
$(subdir-y):
make -C $@ -f $(TOPDIR)/Makefile.build

# 把subdir的built-in.o和cur_objs链接成 总的built-in.o目标
built-in.o : $(cur_objs) $(subdir_objs)
$(LD) -r -o $@ $^

dep_file = .$@.d

# 生成 cur_objs 目标
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<

.PHONY : $(PHONY)

子目录下 makefile，用于添加 obj-y 信息

Copy
# 可以不添加下面两个变量
EXTRA_CFLAGS :=
CFLAGS_view.o :=

obj-y += $(patsubst %.c,%.o,$(shell ls *.c))
# obj-y += subdir/

自动化构建工具需要实现：
1）工程从未被编译，则所有文件都要编译，链接
2）工程的某几个文件被修改，则只编译修改的文件，并链接至目标程序
3）工程的头文件被改变，则编译引用了这几个头文件的 c 文件，并链接目标程序
*）打包一些 shell 命令，方便工程管理，比如打包，备份，清理中间文件

make 运行

make 启动

默认脚本优先级：GNUmakefile > makefile > Makefile

Copy
make -f custom.mk #指定入口makefile文件

command 打印

make 执行 command 时会把 command 打印出来；用 @ 字符在命令前，则不会打印出来；
比如echo 正在编译XXX模块，终端显示：
echo 正在编译XXX模块 正在编译XXX模块
如果是@echo 正在编译XXX模块就不会输出

Copy
make -s #静默执行command

command 运行的 shell 环境

make 命令默认使用环境变量 SHELL 指定的 Shell 执行，用户没指定SHELL时， Unix Shell 默认是 $(SHELL)=/bin/sh;Windows 环境下，make 会自动寻找合适的命令解释器，例如 cmd.exe 或者你指定的 Unix 风格 Shell（如 bash）

Copy
-------------------让上一条命令的结果应用在下一条命令时，用分号分隔这两条命令。分行写，则前一命令的效果不会保留
.RECIPEPREFIX = >
exec:
> cd /home/hchen; pwd
# 打印 /home/hchen

.RECIPEPREFIX = >
exec:
> cd /home/hchen
> pwd
# 打印 /home

make 命令失败处理

Copy
————————忽略错误，不终止 make 的执行
clean:
-rm -f *.o #command前加减号

.IGNORE: clean #特殊目标.IGNORE指定clean规则忽略错误，不终止 make 的执行
clean:
rm -f *.o

make -i #用 -i 或 --ignore-errors 参数选项
make --ignore-errors

————————只跳过当前失败的规则，继续执行其它规则，不会终止 make 执行
make -k
make --keep-going

make 查看规则

Copy
make -p # 输出所有的规则和变量。
make -n # 打印将要执行的规则和命令，但不执行
make --debug=verbose #
make -t # 相当于UNIX touch，把目标的修改日期变成最新的，相当于假编译，只是把目标变成已编译状态
make -q # 检查target是否存在，返回码指示结果（0=需更新，2=错误）。
make -W <file> # 一般指定源文件（或依赖文件），Make根据规则推导运行依赖于这个文件的命令，一般和-n一同使用查看此文件所发生的规则命令

make 退出

Make 退出码:
1)0：执行成功。
2)1：执行中出现错误。
3)2：-q选项启用时，目标不需要更新

make 参数

选项 | 功能
-b, -m | 忽略与其他版本兼容性相关的警告。
-B, --always-make | 强制重编译所有目标。
-C | 切换到指定目录运行 Makefile。
--debug[=<options>] | 输出调试信息，<options>包括 all、basic、verbose、implicit、jobs、makefile。
-d | 等同于 --debug=all
-e, --environment-overrides | 环境变量覆盖 Makefile 中定义的变量。
-f=<file> | 指定 Makefile 文件。
-h | 显示帮助信息
-i, --ignore-errors | 忽略所有错误。
-I | 指定一个可导入 makefile 的搜索路径。可以使用多个 "-I" 参数来指定多个目录
-j [<jobsnum>] | 指定最大并行执行任务数。
-k, --keep-going | 忽略失败的目标，继续处理其他目标。
-l <load> | 指定允许的最大负载值，超出则暂停新的任务。
-n | 打印将要执行的规则和命令，但不执行
-o <file> | 不重新生成的指定的<file>，即使这个目标的依赖文件新于它。
-p, --print-data-base | 输出所有的规则和变量。
-q, --question | 检查 target 是否存在，返回码指示结果（0 = 需更新，2 = 错误）。
-r | 禁用所有内置规则。
-R | 禁用所有内置变量。
-s, --silent | 禁止命令输出。
-S, --no-keep-going | 禁用 -k 参数的作用
-t | 相当于 UNIX touch，把目标的修改日期变成最新的，相当于假编译，只是把目标变成已编译状态
-v, --version | 显示版本信息。
-w, --print-directory | 显示当前目录及嵌套调用信息。
--no-print-directory | 禁止 "-w" 选项
-W <file> | 一般指定源文件 (或依赖文件)，Make 根据规则推导运行依赖于这个文件的命令，一般和 - n 一同使用查看此文件所发生的规则命令
--warn-undefined-variables | 警告未定义的变量。

规则

一条构建规则由依赖关系和命令组成：
target：要生成的目标文件、可执行文件或 label
prerequisites：目标文件依赖的源文件或中间文件
command：调用的 shell 命令；通常是构建工具命令；一定要以一个 制表符Tab 作为开头；或者 command 跟依赖关系在同一行，用;隔开

执行 make 命令时，Make 会：
1）查找当前目录下的 Makefile 或 makefile 文件。也可指定路径 make -f ~/makefile
2）查找 Makefile 中第一个 target；也可指定目标make target
3）检查 target 的 prerequisites（.c/.o）是否需更新。如 prerequisites 比 target 新，或 target 不存在，则执行 command
4）prerequisites (.o) 可能本身也作为 target 依赖其他文件(.c），Make 会递归追踪 .o 文件的依赖，直到最终的源文件 .c 被编译。
5）编译错误或文件缺失，则 Make 报错并终止执行；在命令前 +- 忽略文件未找到的警告并继续执行

如： file.c 文件被修改， file.o 会重新编译，且 edit 会重新链接

Copy
target: prerequisites; command; command
command
command

嵌套执行 make

大型项目中，将代码划分为多个模块或子目录，每个子目录维护独立的 Makefile 是一种常见的工程管理方式。这种方法使每个模块的编译规则更加清晰，简化了 Makefile 的维护。

用到的 shell 变量：
MAKE：GNU make 中的一个特殊变量，用于递归调用 make 自身；假设在 shell 执行make -j4，总控 Makefile 有$(MAKE) -C subdir；则相当于在 subdir 执行make -j4 -C subdir
MAKEFLAGS ：系统级变量，会自动传递到下级 Makefile，可通过设置为空来阻止传递（MAKEFLAGS=）
MAKELEVEL : 系统变量，记录当前 Make 执行的嵌套层数。如果你有嵌套的 make 调用，MAKELEVEL 会告诉你当前是在第几层。

假设有一个子目录叫 subdir，这个目录下有个 Makefile 文件，来指明了这个目录下文件的编译规则。

总控 Makefile

Copy
.RECIPEPREFIX = >
#如果当前 make 在最外层执行（MAKELEVEL= 0）；定义一些与系统相关的变量，
ifeq (0,${MAKELEVEL})
cur-dir := $(shell pwd)
whoami := $(shell whoami) #当前用户
host-type := $(shell arch) #架构
MAKE := ${MAKE} host-type=${host-type} whoami=${whoami}
endif

.PHONY: all subsystem

all: subsystem #all依赖于subsystem

subsystem:
> $(MAKE) -C subdir MAKEFLAGS= VAR=value # -C subdir 表示在指定目录中执行 make
#或> cd subdir && $(MAKE)

subdir/Makefile

Copy
.PHONY: all

all:
echo "Received VAR = $(VAR)"

例子的执行结果：

-w 或 --print-directory 是调试嵌套 Makefile 的好帮手，它会输出当前的工作目录信息。如果使用 -C 参数，-w 会自动启用。如果包含 -s 或 --silent 参数，则 -w 会失效。

Copy
make: Entering directory `subdir'
Received VAR = value
make: Leaving directory `subdir'

命令包

Copy
define <命令包名>
<command1>
<command2>
...
endef

-------eg
.RECIPEPREFIX = >

define run-yacc
yacc $(firstword $^) # $(firstword $^) 会获取 $^ 中的第一个依赖文件。
mv y.tab.c $@ # 将生成的 y.tab.c 文件重命名为 foo.c
endef

foo.c : foo.y
> $(run-yacc)

变量

变量用于存储字符串，类似于 C/C++ 的宏；能提高脚本的可维护性，避免重复代码；eg：只需维护 objects 变量，不必修改规则；

命名规则： 可包含字母、数字、下划线 (_)，且可以以数字开头。大小写敏感；系统变量通常全大写，如 CC、CFLAGS。用户自定义变量建议驼峰命名，如 MyFlags
声明：VAR = value
引用：$(VAR)

Copy
#使命令前缀变为 >，而不是默认的制表符（tab）
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

#第一个目标
edit: $(objects)
cc -o edit $(objects)

# 各个目标文件的规则
main.o: main.c defs.h
cc -c main.c
kbd.o: kbd.c defs.h command.h
cc -c kbd.c
# 其他 .o 文件的规则...

自动变量

| 含义 | eg
$@ | 依次从 target 集中取出 target，并执于命令 |
$< | 表示依次从依赖集中取出依赖文件，并执于命令 |
$^ | 所有依赖文件的集合（去重） |
$+ | 所有依赖文件的集合（不去重） |
$? | 所有比target新的依赖文件 | target: dependency1 dependency2 command $? 如 dependency1 比 target 新，则 $? 为 dependency1
$* | 依次从格式规则的 target 集中取出 target，去除后缀，剩路径部分 | target: %.c command $* 如果目标是 file.c，则 $* 为 file。
$% | 仅当目标是静态库时，表示依次取出库中的成员 | libfoo.a(bar.o): bar.o ar r $@ $% 如果目标是静态库 libfoo.a，则 $% 是库中的成员 bar.o。
$(@D) | 依次从 target 集中取出 target 的目录部分，并执于命令 | target = dir/subdir/foo.o $(target): @echo $(@D) # dir/subdir
$(@F) | 依次从 target 集中取出 target 的文件名部分，并执于命令 | target = dir/subdir/foo.o $(target): @echo $(@F) # foo.o
$(<D) | 依次从依赖集中取出依赖文件的目录部分，并执于命令 | target: dir/file.c command $(<D) 如果第一个依赖文件是 dir/file.c，则 $(<D) 为 dir。
$(<F) | 依次从依赖集中取出依赖文件的文件名部分，并执于命令 | target: dir/file.c command $(<F) 如果第一个依赖文件是 dir/file.c，则 $(<F) 为 file.c。
$(^D) | 所有依赖文件的目录部分的集合（去重） | target: dir/file1.c dir/file2.c command $(^D) 如果依赖文件是 dir/file1.c 和 dir/file2.c，则 $(^D) 为 dir。
$(^F) | 所有依赖文件的文件名部分的集合（去重） | target: dir/file1.c dir/file2.c command $(^F) 如依赖文件是 dir/file1.c 和 dir/file2.c，则 $(^F) 为 file1.c file2.c
$(+D) | 所有依赖文件的目录部分的集合（不去重） | target: dir/file1.c dir/file2.c dir/file1.c command $(+D) 依赖文件是 dir/file1.c dir/file2.c dir/file1.c，则 $(+D) 为 dir dir dir
$(+F) | 所有依赖文件的文件部分的集合（不去重） | target: dir/file1.c dir/file2.c dir/file1.c command $(+F) 依赖文件是 dir/file1.c dir/file2.c dir/file1.c，则 $(+F) 为 file1.c file2.c file1.c
$(?D) | 被更新过的依赖文件的目录部分 | target: file1.c file2.c command $(?D) 如果 file1.c 更新，则 $(?D) 为 file1。
$(?F) | 被更新过的依赖文件的文件部分 | target: file1.c file2.c command $(?F) 如果 file1.c 更新，则 $(?F) 为 file1.c。
$(*D) | 依次从格式规则的 target 集中取出 target，去除后缀和文件名部分 | %.o: %.c @echo $(*D)
$(*F) | 依次从格式规则的 target 集中取出 target，去除后缀和目录部分 | %.o: %.c @echo $(*F)

静态模式规则：
1）<targets ...> 目标集，可以有通配符
2）目标集文件格式，从 <targets ...> 中匹配出符合的新目标集
3）<prereq-patterns ...> 依赖文件集格式，匹配新目标集 的依赖集

Copy
.RECIPEPREFIX = >
bigoutput littleoutput: text.g
> generate text.g -$(subst output,,$@) > $@ # 参数替换：$(subst output,,$@) 表示替换 $@ 中的 output 为空字符串

#等价于上
.RECIPEPREFIX = >
bigoutput: text.g
> generate text.g -big > bigoutput

littleoutput: text.g
> generate text.g -little > littleoutput

-------------------------------------静态模式规则
.RECIPEPREFIX = >
<targets ...> : <target-pattern> : <prereq-patterns ...>
> <commands>

#eg
.RECIPEPREFIX = >
objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c #匹配 %.o格式 的目标文件，由$@逐个取出；匹配目标集的 依赖集文件格式为%.c ，由$< 逐个取出
> $(CC) -c $(CFLAGS) $< -o $@

#等同于上
foo.o: foo.c
> $(CC) -c $(CFLAGS) foo.c -o foo.o

bar.o: bar.c
> $(CC) -c $(CFLAGS) bar.c -o bar.o

命令行环境变量

除了 **Makefile 内部定义的变量；makefile 能以 $(var) 的方式引用 1）os环境变量和 2）make 选项传递的命令行环境变量；

同名变量优先级：命令行传递的变量值 >Makefile 中定义的变量值 > os 环境变量
1）提升 ****Makefile**** 中定义的变量值优先级：override 变量前缀
2）提升 os 环境变量优先级：make -e**
**
eg：make CFLAGS="-O2 -Wall"传递CFLAGS** 环境变量；export CC=gcc传递 os 环境变量 CC

Copy
# Makefile
all:
echo $(CFLAGS) #传递进来了-O2 -Wall
echo $(CC)

$$ 在 bash 中代表当前进程的 PID（进程 ID）
$$$$ 会插入当前进程的 PID 的最后四个数字。通常用于生成唯一的临时文件名

在 makefile 间传递变量

Copy
export # 传递所有变量
export variable += value; # 传递变量variable到下级Makefile
unexport variable := value; #不想让变量variable传递到下级Makefile

赋值

变量的值可依赖于其他变量的值

Copy
-----------------= 递归赋值：以递归的方式展开依赖的变量值；这种赋值方法灵活，可推迟变量的定义,缺点是可以写出无限递归赋值，而且每次依赖的变量重新赋值都要重新计算
all:
echo $(foo)

foo = $(bar)
bar = $(ugh)
ugh = Huh?

-----------------:= 立即赋值：在赋值时展开右侧表达式，并将结果存储到变量中，比递归赋值性能更高。更安全
x := foo
y := $(x) bar # foo bar；由于立即展开了，下面一句不影响y的值
x := later

-----------------?= 条件赋值：只有未定义变量，赋值才生效；已定义变量则什么也不做，而非覆盖原定义
FOO ?= bar

-----------------# 不仅能用来注释，还可用来标示变量定义的结束位置
dir := /foo/bar # directory to put the frobs in

-----------------+= 追加变量值,原变量用 := 定义，则 +=为立即赋值;变量用 = 定义，则 +=为递归赋值
objects = main.o foo.o bar.o utils.o
objects += another.o #相当于objects = $(objects) another.o

-----------------指定makefile中定义的某些变量有最高优先级，不会被make选项传入的命令行变量覆写
override <variable> := <value>
override <variable> += <more text>
override <variable> = <value>

-----------------多行变量定义：跟命令包一样
define <variable>
<value>
endef

变量值替换

Copy
-----------------简单替换
${var:a=b} #将变量 var 中以 a 结尾的部分替换为 b

foo := a.o b.o c.o
bar := $(foo:.o=.c)

-----------------模式替换
$(var:%.suffix1=%.suffix2) # 将变量var中 %.suffix1 替换为 %.suffix2

foo := a.o b.o c.o
bar := $(foo:%.o=%.c)

Target-specific Variable

变量只对特定目标及其依赖有效，避免影响到其他目标的设置

Copy
-----------------------------------------变量表达式只对特定目标<target>及其依赖有效
<target> : <variable-assignment> # 变量表达式只对特定目标<target>及其依赖有效
<target> : override <variable-assignment>
#eg:不论全局 CFLAGS 的值是什么，prog 目标及其相关的目标（prog.o、foo.o、bar.o）会使用 CFLAGS = -g
prog : CFLAGS = -g
prog : prog.o foo.o bar.o
$(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
$(CC) $(CFLAGS) prog.c

foo.o : foo.c
$(CC) $(CFLAGS) foo.c

bar.o : bar.c
$(CC) $(CFLAGS) bar.c

---------------------------变量表达式只对特定格式目标有效
<pattern ...>; : <variable-assignment>;
<pattern ...>; : override <variable-assignment>;
%.o : CFLAGS = -O2 #变量表达式只对.o结尾的目标有效

动态变量名

Copy
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c) #$(dir)_sources定义为$(dir)目录下所有c文件
define $(dir)_print #甚至是动态定义命令包
lpr $($(dir)_sources) #打印$(dir)_sources的值
endef

特殊值的转义

$$Makefile 中，单个 $ 被用于变量引用。而任何 $$会视为$ 字符的转义

Copy
-----------------定义包含空格的变量
nullstring :=
space := $(nullstring)

文件路径搜索

shell 通配符：
*(匹配任意长度字符)
? (匹配一个字符)
~ (家目录或环境变量)

如果没有指明特殊变量 VPATH ，make 只会在当前目录中去找寻依赖文件和目标文件。如果定义了这个变量，那么，make 就会在当前目录找不到的情况下，到所指定的目录中去找寻文件

Copy
VPATH = src:../headers # 现在当前目录下寻找。再去src，以及../headers找

vpath <pattern> <directories>
vpath %.h ../headers #当前目录没找到时；在../headers"目录下搜索所有以 .h 结尾的文件；..上级目录
vpath %.c foo #.c 文件，按"foo""blish""bar"目录顺序搜索
vpath % blish
vpath %.c bar

objects := $(wildcard *.o) # 展开通配符*.o通配符,列出所有 .o 文件

$(patsubst %.c,%.o,$(wildcard *.c)) # 将%.c字符串替换为%.o字符串，列出所有.c文件对应的 .o 文件

objects := $(patsubst %.c,%.o,$(wildcard *.c)) # 编译并链接所有 .c 和 .o 文件
foo : $(objects)
> cc -o foo $(objects)

隐含规则

只需编写项目链接环节的规则，而从源文件到目标文件的构建规则可由make 根据 1）内置规则库，用户自定义的 2）模式规则和 3）后缀规则 自动推导；

隐含规则优先级：规则按内置顺序匹配，优先级靠前的规则会覆盖靠后的；make 允许重载默认的隐含规则；一般以 Pattern Rules 的形式重载。

以下是常用内置自动推导规则：

target | prerequisites | command
<n>.o | <n>.c | $(CC) -c $(CFLAGS) $(CPPFLAGS)
<n>.o | <n>.cc / <n>.C | $(CXX) -c $(CXXFLAGS) $(CPPFLAGS)
<n> | <n>.o | $(CC) $(LDFLAGS) <n>.o $(LDLIBS)
<n>.c | <n>.y | $(YACC) $(YFLAGS)
<n>.c | <n>.l | $(LEX) $(LFLAGS)

以及默认变量：

命令变量 | 描述 | 默认值
AR | 函数库打包程序 | ar
AS | 汇编语言编译器 | as
CC | C 语言编译器 | cc
CXX | C++ 语言编译器 | g++
CO | 从 RCS 文件中扩展文件程序 | co
CPP | C 程序的预处理器 | $(CC) -E
FC | Fortran 和 Ratfor 编译器 | f77
LEX | Lex 文法分析器（针对 C 或 Ratfor） | lex
PC | Pascal 编译器 | pc
YACC | Yacc 文法分析器（针对 C 程序） | yacc
MAKEINFO | 将 Texinfo 文件转换为 Info 格式的程序 | makeinfo

参数变量 | 描述 | 默认值
ARFLAGS | AR 命令的参数 | rv
ASFLAGS | 汇编器的参数 | 空
CFLAGS | C 语言编译器的参数 | 空
CXXFLAGS | C++ 语言编译器的参数 | 空
COFLAGS | RCS 命令的参数 | 空
CPPFLAGS | C 预处理器的参数 | 空
FFLAGS | Fortran 编译器的参数 | 空
LDFLAGS | 链接器的参数 | 空
LFLAGS | Lex 文法分析器的参数 | 空
PFLAGS | Pascal 编译器的参数 | 空
RFLAGS | Ratfor 编译器的参数 | 空
YFLAGS | Yacc 文法分析器的参数 | 空

自动推导构建规则

GNUmake 可自动推导某些 prerequisites 到 target 的构建规则，比如 C 项目每个 x.o 文件后必然有一个同名x.c依赖，必然有一条cc -c x.c；

自动推导有个问题就是规则匹配优先级：比如你显式指定foo.o : foo.p; 但文件目录下存在foo.c；此时会自动推导出foo.c构建foo.o的规则；make -r 或 make --no-builtin-rules 能禁用所有内置隐含规则。添加明确规则可以覆盖隐含规则。

Copy
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit: $(objects)
cc -o edit $(objects)

# 自动推导规则，make 会推导出这些规则
main.o: defs.h
kbd.o: defs.h command.h
command.o: defs.h command.h
# 其他规则...

另一种风格：其实就是进一步把相同.h文件的目标合并表示；我不喜欢

Copy
.RECIPEPREFIX = >
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o

edit: $(objects)
cc -o edit $(objects)

# 将多个文件的依赖合并
$(objects): defs.h
kbd.o command.o files.o: command.h
display.o insert.o search.o files.o: buffer.h

.PHONY: clean
clean:
rm edit $(objects)

Pattern Rules

用通用的格式描述一组目标和依赖文件的关系。在目标或依赖文件中用 % 代表任意字符或文件名的一部分

Copy
#eg:用Pattern Rules重载内建隐含的.c构建.o规则
%.o : %.c
$(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@

%.o : %.s #取消内建的隐含规则，只要不在后面写命令就行

后缀规则

保持兼容的老规则不推荐，遇到这么写的项目再看

引用其它的 Makefile

将其它 Makefile 文件的规则和变量加载到当前 Makefile ; 用 include 引入文件时，Make 会在以下几个位置查找文件：
1) 当前目录
2) 用 -I 指定的目录
3) 默认的系统目录（如 /usr/include）

Copy
-include other.make *.mk $(othermake) #可以是文件名，也可用通配符

sinclude other.make *.mk $(othermake) #同上

伪目标

指明 target 不是文件，而是 make 后跟的操作标签。用于打包 shell 命令；伪目标不能和文件同名，为确保这一点，用.PHONY显式声明，使存在同名文件时伪目标也能执行；

伪目标可依赖其他伪目标，形成分层结构。按深度优先序执行操作；

GNU 惯例：
1）all: 默认目标，编译所有内容。
2）clean: 删除所有生成的文件，包括目标文件和压缩文件。
3）install: 将编译好的程序安装到系统路径中。
4）print: 列出自上次构建以来被修改过的源文件。
5）tar: 打包源程序和相关文件，生成.tar文件。
6）dist: 压缩 tar 文件，生成.tar.gz文件。
7）TAGS: 一个占位规则，可以集成ctags或etags来生成代码索引。
8）check/test: 运行测试，通常可以链接到单元测试框架或脚本。

Copy
.RECIPEPREFIX = >
.PHONY: cleanall cleanobj cleandiff

cleanall: cleanobj cleandiff
> rm -f program

cleanobj:
> rm -f *.o

cleandiff:
> rm -f *.diff

自动生成依赖关系

make 可根据依赖关系判断哪些.c.h 文件修改了需要重新编译；自动生成依赖避免大型项目的手动管理依赖苦难；
gcc -MM file.c生成依赖关系：file.o: file.c defs.h gcc -M main.c生成依赖关系（含标准库头文件）：main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \ /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
GNU 组织建议为为每个 name.c 文件生成一个 name.d 的 Makefile， .d 存放对应 .c 的依赖关系。让 make 自动更新或生成 .d 文件，并把其包含在主 Makefile 中，自动化生成每个文件的依赖关系
sed 's,pattern,replacement,g'：字符串替换命令
1）pattern ：要匹配的内容；\($*\)\.o[ :]*；$表示匹配零个或多个字符，* 表示前面元素出现零次或多次，捕获组\(\)中键入$*表示字符串会被捕获到 \(\)；\.o表示匹配.o；[ :]*表示可匹配多个 :
2）replacement ：正则匹配替换成的内容；\1.o $@ :；\1 表示第一个捕获组；$@**** 表示Makefile 规则中的 target；替换成main.o main.d :；于是， .d 文件也会自动更新和自动生成了
3）g ：表示替换所有匹配的地方。

Copy
.RECIPEPREFIX = >

sources = foo.c bar.c

%.d: %.c
> @set -e; rm -f $@; \ # @set -e 设置脚本一旦遇到错误（命令返回值非零）立即退出；rm -f $@删除已存在的中间文件
$(CC) -M $(CPPFLAGS) $< > $@.$$$$; \ # 由%.c逐个构建依赖关系，重定向到一个临时文件$@.$$$$，相当于%.d.$$$$
sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \ # < $@.$$$$表示从文件 %.d.$$$$ 中读取输入；替换后的内容输出到%.d；这一步是为子.d自动更新
rm -f $@.$$$$

include $(sources:.c=.d) # 把变量 $(sources) 所有 .c 的字串都替换成 .d；然后把依赖关系include进来

中间目标处理

Copy
.INTERMEDIATE: mid #显式声明mid文件是中间目标。make不会把它当最终目标处理,当生成mid文件后不会被自动删除
.SECONDARY: sec #sec 文件是一个中间目标，但 make 不会在生成最终目标后删除它
.PRECIOUS: %.o #告诉 make 保留所有 .o 文件，即使它们是中间目标

条件表达式

Copy
<conditional-directive>
<text-if-true> #
else
<text-if-false>
endif

-----------------ifeq 和 ifneq
.RECIPEPREFIX = >
foo = bar

ifeq ($(foo),bar)
> echo "foo is bar"
endif

ifneq ($(foo),baz)
> echo "foo is not baz"
endif

-----------------ifdef 和 ifndef
.RECIPEPREFIX = >
foo = bar

ifdef foo
> echo "foo is defined"
else
> echo "foo is not defined"
endif

函数

函数调用语法

Copy
$(<function> <arguments>)
${<function> <arguments>}

#<function> 是函数名;<arguments> 是参数，用逗号分隔;函数名和参数之间用空格隔开

字符串处理

Copy
$(subst <from>,<to>,<text>) # 字符串替换:把字符串<text>中的<from>替换为<to>；并返回
#eg: $(subst ee,EE,feet on the street)

$(patsubst <pattern>,<replacement>,<text>) # 字符串替换：把字符串<text>中的<pattern>替换为<replacement>；并返回
#eg: $(patsubst %.c,%.o,x.c.c bar.c)

$(strip <string>) # 去掉字符串开头和结尾的空格
#eg: $(strip a b c )

$(findstring <sub_text>,<text>) # 在字符串<text>中查找子串<sub_text>
#eg: $(findstring a,a b c) # 返回值：a

$(filter <pattern...>,<text>) # 过滤出<text>中符合格式 <pattern...>的部分
#eg: $(filter %.c %.s,bar.c baz.s ugh.h)

$(filter-out <pattern...>,<text>) # 过滤出<text>中不符合格式 <pattern...>的部分
#eg: $(filter-out main1.o main2.o,main1.o foo.o main2.o)

$(sort ) # 排序并去重
#eg: $(sort foo bar lose foo)

$(word <n>,<text>) # 提取<text>中第n个单词
#eg: $(word 2,foo bar baz) #取出了bar

$(wordlist <start>,<end>,<text>) # 提取单词范围
#eg: $(wordlist 2,3,foo bar baz) # 返回值：bar baz

$(words <text>) # 统计单词数
#eg: $(words foo bar baz) # 返回值：3

$(firstword <text>) # 获取第一个单词
#eg: $(firstword foo bar)

文件名操作

Copy
$(dir <path...>) #提取目录部分
#eg: $(dir src/foo.c hacks) # 返回值：src/ ./

$(notdir <path...>) #提取非目录部分
#eg: $(notdir src/foo.c hacks) # 返回值：foo.c hacks

$(suffix <names...>) # 提取文件后缀
#eg: $(suffix src/foo.c src-1.0/bar.c hacks) # 返回值：.c .c

$(basename <names...>) # 提取文件名前缀
#eg: $(basename src/foo.c src-1.0/bar.c hacks) # 返回值：src/foo src-1.0/bar hacks

$(addsuffix <suffix>,<names...>) # 给<names...>中的文件添加后缀<suffix>
#eg: $(addsuffix .c,foo bar) # 返回值：foo.c bar.c

$(addprefix <prefix>,<names...>) #添加前缀
#eg: $(addprefix src/,foo bar) # 返回值：src/foo src/bar

$(join <list1>,<list2>) #连接两个列表
#eg: $(join aaa bbb,111 222 333) # 返回值：aaa111 bbb222 333

高级函数

Copy
$(foreach , ,<text>) # 循环处理列表:从 依次取出单词赋值给，然后用表达式<text>进行计算，最终返回由每次计算结果用空格连起来的字符串
#eg: files := $(foreach n,a b c d,$(n).o) # 返回值：a.o b.o c.o d.o

$(if <condition>,<then-part>,<else-part>) #条件判断
#eg: $(if 1,yes,no) #yes

$(call <expression>,<parm1>,<parm2>,...) #自定义函数调用:用占位符 $(1)、$(2) 等表示参数,传递到<expression>计算并返回
#eg:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
# 返回值：b a

$(origin <variable>) #检查变量来源
#返回值：
undefined： 未定义
default： 默认定义
environment： 环境变量
file： Makefile 定义
command line： 命令行定义
override： override定义
automatic： 自动变量

$(shell <command>) #执行Shell命令
#eg: files := $(shell echo *.c)

$(error <text...>) #终止 make 的执行，并显示指定的错误消息
#eg: 检查变量是否为空
MY_VAR :=

ifeq ($(MY_VAR),)
$(error MY_VAR is not defined or is empty)
endif

$(warning <text...>) #输出警告，但不会中断构建过程
#eg: 输出调试信息
MY_VAR :=

ifeq ($(MY_VAR),)
$(warning MY_VAR is not defined or is empty)
endif

更新函数库文件

-j 并行构建库可能会影响 ar 打包

Copy
#eg:将所有的 .o 文件打包成 foolib 函数库
foolib(*.o) : *.o
ar cr foolib *.o

模板

tree

Copy
<PROJECT_NAME >/
├── build/ # 存放编译生成的目标文件、依赖文件等
├── bin/ # 存放最终生成的可执行文件
├── inc/ # 存放头文件
├── lib/ # 存放库文件
├── src/ # 存放源代码文件 (.c)
├── tests/ # 存放测试代码
├── submodule1/ # 第一个子模块
│ └── Makefile # 子模块的 Makefile
├── submodule2/ # 第二个子模块
│ └── Makefile # 子模块的 Makefile
├── Makefile # 顶层 Makefile
└── <其他文件> # 可能包括其他文档、配置文件等

总控 makefile

删除生成的目标文件，以便重新编译。更稳健的做法是使用 .PHONY 声明，并在删除命令前加上 -，这样即使某些文件不存在，也不会报错：

Copy
.RECIPEPREFIX = >
# -------------------------------
# General settings
# -------------------------------
PROJECT_NAME :=
VERSION := 1.0.0

INC_DIR := inc
SRC_DIR := src
LIB_DIR := lib
BIN_DIR := bin
TEST_DIR := tests
BUILD_DIR := build
SUBMODULES := submodule1 submodule2
INSTALL_DIR := /usr/local/bin
TAR_FILE := $(PROJECT_NAME)-$(VERSION).tar.gz

# -------------------------------
# Compiler and flags
# -------------------------------
CC := cc
CXX := g++
AR := ar
AS := as
CO := co
CPP := $(CC) -E
LEX := lex
YACC := yacc
MAKEINFO := makeinfo

CFLAGS := -Wall -Wextra -Og
LIBS :=
ARFLAGS := rv
ASFLAGS :=
CXXFLAGS :=
COFLAGS :=
CPPFLAGS :=
LDFLAGS :=
LFLAGS :=
YFLAGS :=

# -------------------------------
# 非独立构建加载子模块Makefile（即把子模块当静态库和源代码用）；各模块独立构建则不需要
# -------------------------------
#-include $(foreach submodule, $(SUBMODULES), $(submodule)/Makefile)

# -------------------------------
# top level Files
# -------------------------------
SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
DEPS := $(OBJS:.o=.d)

# -------------------------------
# Targets
# -------------------------------
.PHONY: all clean install print tar dist TAGS test $(SUBMODULES) #还应该加个检查是否安装相关工具的目标

all: $(PROJECT_NAME)

# Build the main target executable
$(PROJECT_NAME): $(OBJS) $(SUBMODULES)
> $(CC) $(OBJS) -o $(BIN_DIR)/$@ $(LDFLAGS) $(LIBS)

# 构建子模块:遍历子模块，进入其中执行make；如果找不到makefile，就打印skip
$(SUBMODULES):
> @$(foreach submodule, $(SUBMODULES), \
$(MAKE) -C $(submodule) || echo "Skipping $(submodule)";)

# Automatically generate dependency files
-include $(DEPS)

# Clean target: remove build artifacts
clean:
> rm -rf $(BUILD_DIR) $(BIN_DIR) $(LIB_DIR) $(TAR_FILE)
> $(foreach submodule, $(SUBMODULES), $(MAKE) -C $(submodule) clean || echo "Skipping $(submodule)";)

# Install target: install the program
install: all
> install -m 755 $(BIN_DIR)/$(PROJECT_NAME) $(INSTALL_DIR)

# Print target: list modified source files
print:
> @echo "Modified source files since last build:"
> @find $(SRC_DIR) -type f -newer $(BIN_DIR)/$(PROJECT_NAME) -print || echo "No modified source files."

# Tarball target: create a source tarball
tar:
> tar -cvzf $(TAR_FILE) $(SRC_DIR) $(INC_DIR) $(LIB_DIR) Makefile $(SUBMODULES)

# Dist target: compress the tarball
dist: tar
> gzip $(TAR_FILE)

# TAGS target: generate ctags or etags
TAGS:
> ctags -R $(SRC_DIR) $(INC_DIR) $(SUBMODULES)

# Test target: run unit tests (assuming a separate test suite)
test:
> @echo "Running tests..."
> $(MAKE) -C $(TEST_DIR)

# -------------------------------
# Compilation rules
# -------------------------------
# Rule to create object files，这里妙在.c.h更新，则.o更新；.o更新引用这条规则，.d也更新
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MD -MP -c $< -o $@

# Include all dependency files
-include $(DEPS)

独立构建 submodule makefile

Copy

非独立构建 submodule makefile

Copy
.RECIPEPREFIX = >

# 子模块配置
MODULE_NAME := submodule1
SRC_DIR := src
INC_DIR := inc
BUILD_DIR := build
LIB_DIR := ../lib

CC := cc
CFLAGS := -Wall -Wextra -Og -I$(INC_DIR)
AR := ar
ARFLAGS := rv

SRCS := $(shell find $(SRC_DIR) -name '*.c')
OBJS := $(patsubst $(SRC_DIR)/%.c, $(BUILD_DIR)/%.o, $(SRCS))
LIB := $(LIB_DIR)/lib$(MODULE_NAME).a
DEPS := $(OBJS:.o=.d)

.PHONY: all clean

# 默认目标
all: $(LIB)

# 生成静态库
$(LIB): $(OBJS)
> @mkdir -p $(LIB_DIR)
> $(AR) $(ARFLAGS) $@ $^

# 编译规则
$(BUILD_DIR)/%.o: $(SRC_DIR)/%.c
> @mkdir -p $(@D)
> $(CC) $(CFLAGS) -MMD -MP -c $< -o $@

# 清理规则
clean:
> rm -rf $(BUILD_DIR) $(LIB)

# 自动包含依赖文件
-include $(DEPS)