# 伪目标
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 = [email protected]
# 生成 cur_objs 目标
%.o : %.c
$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
.PHONY : $(PHONY)
子目录下 makefile,用于添加 obj-y 信息#
# 可以不添加下面两个变量
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
make -f custom.mk #指定入口makefile文件
command 打印#
make 执行 command 时会把 command 打印出来;用 @
字符在命令前,则不会打印出来;
比如echo 正在编译XXX模块
,终端显示:
echo 正在编译XXX模块 正在编译XXX模块
如果是@echo 正在编译XXX模块
就不会输出
make -s #静默执行command
command 运行的 shell 环境#
make 命令默认使用环境变量 SHELL
指定的 Shell 执行,用户没指定SHELL
时, Unix Shell 默认是 $(SHELL)
=/bin/sh
;Windows 环境下,make
会自动寻找合适的命令解释器,例如 cmd.exe
或者你指定的 Unix 风格 Shell(如 bash
)
-------------------让上一条命令的结果应用在下一条命令时,用分号分隔这两条命令。分行写,则前一命令的效果不会保留
.RECIPEPREFIX = >
exec:
> cd /home/hchen; pwd
# 打印 /home/hchen
.RECIPEPREFIX = >
exec:
> cd /home/hchen
> pwd
# 打印 /home
make 命令失败处理#
————————忽略错误,不终止 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 查看规则#
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 <dir> | 切换到指定目录运行 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 <dir> | 指定一个可导入 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
会重新链接
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#
.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#
.PHONY: all
all:
echo "Received VAR = $(VAR)"
例子的执行结果:
-w
或 --print-directory
是调试嵌套 Makefile
的好帮手,它会输出当前的工作目录信息。如果使用 -C
参数,-w
会自动启用。如果包含 -s
或 --silent
参数,则 -w
会失效。
make: Entering directory `subdir'
Received VAR = value
make: Leaving directory `subdir'
命令包#
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)
#使命令前缀变为 >,而不是默认的制表符(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 ...> 依赖文件集格式,匹配新目标集 的依赖集
.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
# Makefile
all:
echo $(CFLAGS) #传递进来了-O2 -Wall
echo $(CC)
$$
在 bash
中代表当前进程的 PID(进程 ID)
$$$$
会插入当前进程的 PID 的最后四个数字。通常用于生成唯一的临时文件名
在 makefile 间传递变量#
export # 传递所有变量
export variable += value; # 传递变量variable到下级Makefile
unexport variable := value; #不想让变量variable传递到下级Makefile
赋值#
变量的值可依赖于其他变量的值
-----------------= 递归赋值:以递归的方式展开依赖的变量值;这种赋值方法灵活,可推迟变量的定义,缺点是可以写出无限递归赋值,而且每次依赖的变量重新赋值都要重新计算
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
变量值替换#
-----------------简单替换
${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
#
变量只对特定目标及其依赖有效,避免影响到其他目标的设置
-----------------------------------------变量表达式只对特定目标<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结尾的目标有效
动态变量名#
dir = foo
$(dir)_sources := $(wildcard $(dir)/*.c) #$(dir)_sources定义为$(dir)目录下所有c文件
define $(dir)_print #甚至是动态定义命令包
lpr $($(dir)_sources) #打印$(dir)_sources的值
endef
特殊值的转义#
$$
Makefile 中,单个 $
被用于变量引用。而任何 $$
会视为$
字符的转义
-----------------定义包含空格的变量
nullstring :=
space := $(nullstring)
文件路径搜索#
shell 通配符:
*(匹配任意长度字符)
? (匹配一个字符)
~ (家目录或环境变量)
如果没有指明特殊变量 VPATH
,make 只会在当前目录中去找寻依赖文件和目标文件。如果定义了这个变量,那么,make 就会在当前目录找不到的情况下,到所指定的目录中去找寻文件
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
能禁用所有内置隐含规则。添加明确规则可以覆盖隐含规则。
.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
文件的目标合并表示;我不喜欢
.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#
用通用的格式描述一组目标和依赖文件的关系。在目标或依赖文件中用 %
代表任意字符或文件名的一部分
#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
)
-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: 运行测试,通常可以链接到单元测试框架或脚本。
.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
:表示替换所有匹配的地方。
.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进来
中间目标处理#
.INTERMEDIATE: mid #显式声明mid文件是中间目标。make不会把它当最终目标处理,当生成mid文件后不会被自动删除
.SECONDARY: sec #sec 文件是一个中间目标,但 make 不会在生成最终目标后删除它
.PRECIOUS: %.o #告诉 make 保留所有 .o 文件,即使它们是中间目标
条件表达式#
<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
函数#
函数调用语法#
$(<function> <arguments>)
${<function> <arguments>}
#<function> 是函数名;<arguments> 是参数,用逗号分隔;函数名和参数之间用空格隔开
字符串处理#
$(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 <list>) # 排序并去重
#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)
文件名操作#
$(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
高级函数#
$(foreach <var>,<list>,<text>) # 循环处理列表:从<list>依次取出单词赋值给<var>,然后用表达式<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 打包
#eg:将所有的 .o 文件打包成 foolib 函数库
foolib(*.o) : *.o
ar cr foolib *.o
模板#
tree#
<PROJECT_NAME >/
├── build/ # 存放编译生成的目标文件、依赖文件等
├── bin/ # 存放最终生成的可执行文件
├── inc/ # 存放头文件
├── lib/ # 存放库文件
├── src/ # 存放源代码文件 (.c)
├── tests/ # 存放测试代码
├── submodule1/ # 第一个子模块
│ └── Makefile # 子模块的 Makefile
├── submodule2/ # 第二个子模块
│ └── Makefile # 子模块的 Makefile
├── Makefile # 顶层 Makefile
└── <其他文件> # 可能包括其他文档、配置文件等
总控 makefile#
删除生成的目标文件,以便重新编译。更稳健的做法是使用 .PHONY
声明,并在删除命令前加上 -
,这样即使某些文件不存在,也不会报错:
.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#
非独立构建 submodule makefile#
.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)