makefile 专题
记录 makefile 基本知识,学习资料来自狄泰软件学院可在淘宝购买学习
一、make 和 makefile
¶1、make 的本质
make 是一个应用程序,用于解析源程序之间的依赖关系,根据依赖关系自动维护编译工作,执行宿主操作系统中的各种命令
¶2、makefile 的本质
makefile 是一个描述文件, 可以 定义一系列的规则来指定源文件编译的先后顺, 拥有特定的语法规则,支持函数定义和函数调用, 能够直接集成操作系统中的各种命令
¶3、make 和 makefile之间的关系
makefile 中的描述用于指导 make 如何完成工作,make 根据 makefile 中的规则执行命令,最后完成编译输出
¶4、最简单的 makefle 程序
1
2
3
4
5
6
7
hello :
@echo “hello makefile”
程序说明:
hello --> 目标
@echo “hello makefile” -> 实现目标所需要执行的命令
注意:目标后的命令需要用tab键(’\t’)隔开!
要运行 makefile 在 linux 下执行以下命令:
1
2
3
make –f mf.txt hello
运行结果:hello makefile
命令功能说明:以 hello 关键字作为目标查找 mf.txt 文件,并执行 hello 目标处的命令
¶5、make 程序简写
上述的命令显示的太长,我们可以对其进行简写,有两种方式:1. make hello, 这句话表示以 hello 为关键字作为目标查找 makeflie 或 Makefile 文件,并执行 hello 处的命令。2. make,这句话表示查找 makefile 或 Makefile 文件中最顶层目标,并执行最顶层目标的命令
二、初识 makefile 的结构
¶1、makefile 的意义
makefile 用于定义源文件之间的依赖关系,说明如何编译各个源文件并生成可执行文件,依赖的定义如下
1
2
targets : prerequisite ; command1
‘\t’ command2 #使用这个方式可以省略分号
targets(目标):
通常是需要生成的目标文件名,make 所需执行的命令名称,可以包含多个目标,使用空格对将多个目标隔开
prerequisite(依赖):
当前目标所依赖的其他目标或文件,可以包含多个依赖,使用空格对将多个依赖隔开,如果省略不写,就意味着只要执行后续的命令那么目标就可以完成了。
command(命令):
完成目标所需要执行的命令,规则中的注意事项,[tab]键:’\t’ ,每一个命令必须以[tab]字符开始,[tab]字符告诉 make 此行是一个命令行。只要命令成功执行完,则认为目标完成。
续行符:‘\’:
可以将内容分开写到下一行,提高可读性
¶3、规则
该图描述了 makefile 的规则,举例说明
1
2
3
4
5
6
7
8
9
all : test
@echo "make all"
test :
@echo "make test"
运行结果:
make test
make all
上述 makefile 定义了两条规则,all 这个目标依赖于 test,如果 test 这个依赖表示的目标成立,就执行 echo “make all” 这个命令,如果 test 不成立,就会以 test 为目标查找其规则。 以 test 为目标的规则没有依赖,因此只需要执行 echo “make test” 命令, test 就会成立。test 成立之后,也就是 all 的依赖完成,因此执行 all 规则的命令 @echo “make all”
¶3、第一个 make 的编译案例
func.c 文件内容如下
1
int i = 5;
main.c 文件内容如下
1
2
3
4
5
6
7
8
9
extern int i;
int main()
{
printf("i = %d\n", i);
return 0;
}
makefile 文件内容如下
1
2
3
4
5
6
7
8
9
10
11
hello.out : main.o func.o
gcc -o hello.out main.o func.o
main.o :
gcc -o main.o -c main.c
func.o :
gcc -o func.o -c func.c
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
小技巧:工程开发中可以将最终可执行文件名和 all 同时作为 makefile 中第一条规则
1
2
hello.out all : main.o func.o
gcc -o hello.out main.o fun.o
当我们想手动编译时执行 make all 就会编译,直接 make 则会检测 hello.out。
三、伪目标
¶1、伪目标的引入
默认情况下,make 认为目标对应着一个文件,make 比较目标文件和依赖文件的新旧关系,决定是否执行命令,make 以文件处理作为第一优先级。但有时候我们希望我们的目录并不是一个文件如下所示:
1
2
clean :
rm main.o func.o
这个时候通过 .PHONY 关键字声明一个伪目标, 伪目标不对应任何实际的文件,不管目标的依赖是否更新,命令总是执行, 伪目标的语法如下
1
.PHONY : Target
伪目标的本质是 make 中特殊目标 .PHONY 的依赖,先声明、后使用。
1
2
3
4
5
6
7
8
.PHONY : clean rebuild all
## other rules ##
rebuile : clean all
clean :
rm *o hello.out
原理:当一个目标的依赖包含伪目标时,伪目标所定义的命令总是会被执行
¶2、绕开 .PHONY 关键字定义伪目标
如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名;在执行此规则时,目标总是被认为是最新的。
1
2
3
clean : FORCE
rm *.o hello.out
FORCE :
四、变量和不同的赋值方式
¶1、makefile中的变量
makefile 中支持程序设计语言中变量的概念,makefile 中变量只代表文本数据。makefile 中变量命名规则如下所示
- 变量名大小写敏感
- 变量名可以包含字符,数字,下划线
- 不能包含 “:” , “#” , “=” , 或 " "
- makefile 未赋值的变量的值为空值
¶2、变量的定义和使用
修改前面的 makefile 验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $(TARGET) main.o func.o
main.o : main.c
$(CC) -o main.o -c main.c
func.o : func.c
$(CC) -o func.o -c func.c
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、makefile中变量的赋值方式
¶1) 简单赋值 :=
程序设计语言中的通用赋值方式, 只针对当前语句变量有效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x := new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = foob
¶2) 递归赋值 =
赋值操作可能影响多个其他变量, 所有与目标变量相关的其他变量将受到影响
1
2
3
4
5
6
7
8
9
10
11
12
x = foo
y = $(x)b
x = new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = newb
¶3) 条件赋值 ?=
如果变量未定义,使用赋值符号中的值定义变量, 如果变量已经定义,赋值无效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x ?= new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo
y = foob
¶4) 追加赋值 +=
原变量值之后加上一个新值, 原变量值与新值之间由空格隔开
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x += $(y)
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo foob
y = foob
五、预定义变量的使用
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PHONY : all first second third
all : first second third
@echo “\$$@ => $@”
@echo “$$^ => $$^”
@echo “$$< => $<”
first :
second :
third :
输出结果:
$@ => all
$^ => first second third
$< => first
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $@ $^
main.o : main.c
$(CC) -o $@ -c $^
func.o : func.c
$(CC) -o $@ -c $^
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.PHONY : all out test first second third
all out : first second third
@echo "$(MAKE)"
@echo "$(MAKECMDGOALS)"
@echo "$(MAKEFILE_LIST)"
first :
@echo "first"
second :
@echo "second"
third :
@echo "third"
test :
@$(MAKE) first
@$(MAKE) second
@$(MAKE) third
运行 make 结果:
first
second
third
make
makefile
运行 make test 结果
make[1]: Entering directory '/home/book/c/mkfile'
first
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
second
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
third
make[1]: Leaving directory '/home/book/c/mkfile'
编程实验 2
1
2
3
4
5
6
7
8
9
10
11
.PHONY : test1 test2
test1 :
@echo "$(MAKE_VERSION)"
@echo "$(CURDIR)"
@echo "$(.VARIABLES)"
运行结果:
4.1
/home/book/c/mkfile
<D ?F .SHELLFLAGS CWEAVE ?D @D @F MAKE_VERSION CURDIR SHELL RM CO COMPILE.mod _ PREPROCESS.F LINK.m LINK.o OUTPUT_OPTION COMPILE.cpp MAKEFILE_LIST GNUMAKEFLAGS LINK.p XDG_DATA_DIRS DBUS_SESSION_BUS_ADDRESS CC CHECKOUT,v LESSOPEN CPP LINK.cc SSH_CONNECTION PATH LD TEXI2DVI YACC SSH_TTY XDG_RUNTIME_DIR ARFLAGS LINK.r LINT COMPILE.f LINT.c YACC.m YACC.y AR .FEATURES TANGLE LS_COLORS GET %F DISPLAY COMPILE.F CTANGLE .LIBPATTERNS LINK.C PWD LINK.S PREPROCESS.r *D LINK.c LINK.s HOME LESSCLOSE LOGNAME ^D MAKELEVEL COMPILE.m MAKE SHLVL AS PREPROCESS.S COMPILE.p XDG_SESSION_ID USER FC .DEFAULT_GOAL %D WEAVE MAKE_COMMAND LINK.cpp F77 OLDPWD .VARIABLES PC *F COMPILE.def LEX ARCH MAKEFLAGS MFLAGS SSH_CLIENT MAIL LEX.l LEX.m +D COMPILE.r MAKE_TERMOUT +F M2C CROSS_COMPILE MAKEFILES COMPILE.cc <F CXX COFLAGS COMPILE.C ^F COMPILE.S LINK.F SUFFIXES COMPILE.c COMPILE.s .INCLUDE_DIRS .RECIPEPREFIX MAKEINFO MAKE_TERMERR OBJC MAKE_HOST TEX LANG TERM F77FLAGS LINK.f
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、make 的本质
make 是一个应用程序,用于解析源程序之间的依赖关系,根据依赖关系自动维护编译工作,执行宿主操作系统中的各种命令
¶2、makefile 的本质
makefile 是一个描述文件, 可以 定义一系列的规则来指定源文件编译的先后顺, 拥有特定的语法规则,支持函数定义和函数调用, 能够直接集成操作系统中的各种命令
¶3、make 和 makefile之间的关系
makefile 中的描述用于指导 make 如何完成工作,make 根据 makefile 中的规则执行命令,最后完成编译输出
¶4、最简单的 makefle 程序
1 | hello : |
注意:目标后的命令需要用tab键(’\t’)隔开!
要运行 makefile 在 linux 下执行以下命令:
1 | make –f mf.txt hello |
命令功能说明:以 hello 关键字作为目标查找 mf.txt 文件,并执行 hello 目标处的命令
¶5、make 程序简写
上述的命令显示的太长,我们可以对其进行简写,有两种方式:1. make hello, 这句话表示以 hello 为关键字作为目标查找 makeflie 或 Makefile 文件,并执行 hello 处的命令。2. make,这句话表示查找 makefile 或 Makefile 文件中最顶层目标,并执行最顶层目标的命令
二、初识 makefile 的结构
¶1、makefile 的意义
makefile 用于定义源文件之间的依赖关系,说明如何编译各个源文件并生成可执行文件,依赖的定义如下
1
2
targets : prerequisite ; command1
‘\t’ command2 #使用这个方式可以省略分号
targets(目标):
通常是需要生成的目标文件名,make 所需执行的命令名称,可以包含多个目标,使用空格对将多个目标隔开
prerequisite(依赖):
当前目标所依赖的其他目标或文件,可以包含多个依赖,使用空格对将多个依赖隔开,如果省略不写,就意味着只要执行后续的命令那么目标就可以完成了。
command(命令):
完成目标所需要执行的命令,规则中的注意事项,[tab]键:’\t’ ,每一个命令必须以[tab]字符开始,[tab]字符告诉 make 此行是一个命令行。只要命令成功执行完,则认为目标完成。
续行符:‘\’:
可以将内容分开写到下一行,提高可读性
¶3、规则
该图描述了 makefile 的规则,举例说明
1
2
3
4
5
6
7
8
9
all : test
@echo "make all"
test :
@echo "make test"
运行结果:
make test
make all
上述 makefile 定义了两条规则,all 这个目标依赖于 test,如果 test 这个依赖表示的目标成立,就执行 echo “make all” 这个命令,如果 test 不成立,就会以 test 为目标查找其规则。 以 test 为目标的规则没有依赖,因此只需要执行 echo “make test” 命令, test 就会成立。test 成立之后,也就是 all 的依赖完成,因此执行 all 规则的命令 @echo “make all”
¶3、第一个 make 的编译案例
func.c 文件内容如下
1
int i = 5;
main.c 文件内容如下
1
2
3
4
5
6
7
8
9
extern int i;
int main()
{
printf("i = %d\n", i);
return 0;
}
makefile 文件内容如下
1
2
3
4
5
6
7
8
9
10
11
hello.out : main.o func.o
gcc -o hello.out main.o func.o
main.o :
gcc -o main.o -c main.c
func.o :
gcc -o func.o -c func.c
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
小技巧:工程开发中可以将最终可执行文件名和 all 同时作为 makefile 中第一条规则
1
2
hello.out all : main.o func.o
gcc -o hello.out main.o fun.o
当我们想手动编译时执行 make all 就会编译,直接 make 则会检测 hello.out。
三、伪目标
¶1、伪目标的引入
默认情况下,make 认为目标对应着一个文件,make 比较目标文件和依赖文件的新旧关系,决定是否执行命令,make 以文件处理作为第一优先级。但有时候我们希望我们的目录并不是一个文件如下所示:
1
2
clean :
rm main.o func.o
这个时候通过 .PHONY 关键字声明一个伪目标, 伪目标不对应任何实际的文件,不管目标的依赖是否更新,命令总是执行, 伪目标的语法如下
1
.PHONY : Target
伪目标的本质是 make 中特殊目标 .PHONY 的依赖,先声明、后使用。
1
2
3
4
5
6
7
8
.PHONY : clean rebuild all
## other rules ##
rebuile : clean all
clean :
rm *o hello.out
原理:当一个目标的依赖包含伪目标时,伪目标所定义的命令总是会被执行
¶2、绕开 .PHONY 关键字定义伪目标
如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名;在执行此规则时,目标总是被认为是最新的。
1
2
3
clean : FORCE
rm *.o hello.out
FORCE :
四、变量和不同的赋值方式
¶1、makefile中的变量
makefile 中支持程序设计语言中变量的概念,makefile 中变量只代表文本数据。makefile 中变量命名规则如下所示
- 变量名大小写敏感
- 变量名可以包含字符,数字,下划线
- 不能包含 “:” , “#” , “=” , 或 " "
- makefile 未赋值的变量的值为空值
¶2、变量的定义和使用
修改前面的 makefile 验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $(TARGET) main.o func.o
main.o : main.c
$(CC) -o main.o -c main.c
func.o : func.c
$(CC) -o func.o -c func.c
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、makefile中变量的赋值方式
¶1) 简单赋值 :=
程序设计语言中的通用赋值方式, 只针对当前语句变量有效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x := new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = foob
¶2) 递归赋值 =
赋值操作可能影响多个其他变量, 所有与目标变量相关的其他变量将受到影响
1
2
3
4
5
6
7
8
9
10
11
12
x = foo
y = $(x)b
x = new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = newb
¶3) 条件赋值 ?=
如果变量未定义,使用赋值符号中的值定义变量, 如果变量已经定义,赋值无效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x ?= new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo
y = foob
¶4) 追加赋值 +=
原变量值之后加上一个新值, 原变量值与新值之间由空格隔开
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x += $(y)
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo foob
y = foob
五、预定义变量的使用
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PHONY : all first second third
all : first second third
@echo “\$$@ => $@”
@echo “$$^ => $$^”
@echo “$$< => $<”
first :
second :
third :
输出结果:
$@ => all
$^ => first second third
$< => first
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $@ $^
main.o : main.c
$(CC) -o $@ -c $^
func.o : func.c
$(CC) -o $@ -c $^
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.PHONY : all out test first second third
all out : first second third
@echo "$(MAKE)"
@echo "$(MAKECMDGOALS)"
@echo "$(MAKEFILE_LIST)"
first :
@echo "first"
second :
@echo "second"
third :
@echo "third"
test :
@$(MAKE) first
@$(MAKE) second
@$(MAKE) third
运行 make 结果:
first
second
third
make
makefile
运行 make test 结果
make[1]: Entering directory '/home/book/c/mkfile'
first
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
second
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
third
make[1]: Leaving directory '/home/book/c/mkfile'
编程实验 2
1
2
3
4
5
6
7
8
9
10
11
.PHONY : test1 test2
test1 :
@echo "$(MAKE_VERSION)"
@echo "$(CURDIR)"
@echo "$(.VARIABLES)"
运行结果:
4.1
/home/book/c/mkfile
<D ?F .SHELLFLAGS CWEAVE ?D @D @F MAKE_VERSION CURDIR SHELL RM CO COMPILE.mod _ PREPROCESS.F LINK.m LINK.o OUTPUT_OPTION COMPILE.cpp MAKEFILE_LIST GNUMAKEFLAGS LINK.p XDG_DATA_DIRS DBUS_SESSION_BUS_ADDRESS CC CHECKOUT,v LESSOPEN CPP LINK.cc SSH_CONNECTION PATH LD TEXI2DVI YACC SSH_TTY XDG_RUNTIME_DIR ARFLAGS LINK.r LINT COMPILE.f LINT.c YACC.m YACC.y AR .FEATURES TANGLE LS_COLORS GET %F DISPLAY COMPILE.F CTANGLE .LIBPATTERNS LINK.C PWD LINK.S PREPROCESS.r *D LINK.c LINK.s HOME LESSCLOSE LOGNAME ^D MAKELEVEL COMPILE.m MAKE SHLVL AS PREPROCESS.S COMPILE.p XDG_SESSION_ID USER FC .DEFAULT_GOAL %D WEAVE MAKE_COMMAND LINK.cpp F77 OLDPWD .VARIABLES PC *F COMPILE.def LEX ARCH MAKEFLAGS MFLAGS SSH_CLIENT MAIL LEX.l LEX.m +D COMPILE.r MAKE_TERMOUT +F M2C CROSS_COMPILE MAKEFILES COMPILE.cc <F CXX COFLAGS COMPILE.C ^F COMPILE.S LINK.F SUFFIXES COMPILE.c COMPILE.s .INCLUDE_DIRS .RECIPEPREFIX MAKEINFO MAKE_TERMERR OBJC MAKE_HOST TEX LANG TERM F77FLAGS LINK.f
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、makefile 的意义
makefile 用于定义源文件之间的依赖关系,说明如何编译各个源文件并生成可执行文件,依赖的定义如下
1 | targets : prerequisite ; command1 |
targets(目标):
通常是需要生成的目标文件名,make 所需执行的命令名称,可以包含多个目标,使用空格对将多个目标隔开
prerequisite(依赖):
当前目标所依赖的其他目标或文件,可以包含多个依赖,使用空格对将多个依赖隔开,如果省略不写,就意味着只要执行后续的命令那么目标就可以完成了。
command(命令):
完成目标所需要执行的命令,规则中的注意事项,[tab]键:’\t’ ,每一个命令必须以[tab]字符开始,[tab]字符告诉 make 此行是一个命令行。只要命令成功执行完,则认为目标完成。
续行符:‘\’:
可以将内容分开写到下一行,提高可读性
¶3、规则
该图描述了 makefile 的规则,举例说明
1 | all : test |
上述 makefile 定义了两条规则,all 这个目标依赖于 test,如果 test 这个依赖表示的目标成立,就执行 echo “make all” 这个命令,如果 test 不成立,就会以 test 为目标查找其规则。 以 test 为目标的规则没有依赖,因此只需要执行 echo “make test” 命令, test 就会成立。test 成立之后,也就是 all 的依赖完成,因此执行 all 规则的命令 @echo “make all”
¶3、第一个 make 的编译案例
func.c 文件内容如下
1 | int i = 5; |
main.c 文件内容如下
1 |
|
makefile 文件内容如下
1 | hello.out : main.o func.o |
小技巧:工程开发中可以将最终可执行文件名和 all 同时作为 makefile 中第一条规则
1 | hello.out all : main.o func.o |
当我们想手动编译时执行 make all 就会编译,直接 make 则会检测 hello.out。
三、伪目标
¶1、伪目标的引入
默认情况下,make 认为目标对应着一个文件,make 比较目标文件和依赖文件的新旧关系,决定是否执行命令,make 以文件处理作为第一优先级。但有时候我们希望我们的目录并不是一个文件如下所示:
1
2
clean :
rm main.o func.o
这个时候通过 .PHONY 关键字声明一个伪目标, 伪目标不对应任何实际的文件,不管目标的依赖是否更新,命令总是执行, 伪目标的语法如下
1
.PHONY : Target
伪目标的本质是 make 中特殊目标 .PHONY 的依赖,先声明、后使用。
1
2
3
4
5
6
7
8
.PHONY : clean rebuild all
## other rules ##
rebuile : clean all
clean :
rm *o hello.out
原理:当一个目标的依赖包含伪目标时,伪目标所定义的命令总是会被执行
¶2、绕开 .PHONY 关键字定义伪目标
如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名;在执行此规则时,目标总是被认为是最新的。
1
2
3
clean : FORCE
rm *.o hello.out
FORCE :
四、变量和不同的赋值方式
¶1、makefile中的变量
makefile 中支持程序设计语言中变量的概念,makefile 中变量只代表文本数据。makefile 中变量命名规则如下所示
- 变量名大小写敏感
- 变量名可以包含字符,数字,下划线
- 不能包含 “:” , “#” , “=” , 或 " "
- makefile 未赋值的变量的值为空值
¶2、变量的定义和使用
修改前面的 makefile 验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $(TARGET) main.o func.o
main.o : main.c
$(CC) -o main.o -c main.c
func.o : func.c
$(CC) -o func.o -c func.c
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、makefile中变量的赋值方式
¶1) 简单赋值 :=
程序设计语言中的通用赋值方式, 只针对当前语句变量有效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x := new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = foob
¶2) 递归赋值 =
赋值操作可能影响多个其他变量, 所有与目标变量相关的其他变量将受到影响
1
2
3
4
5
6
7
8
9
10
11
12
x = foo
y = $(x)b
x = new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = newb
¶3) 条件赋值 ?=
如果变量未定义,使用赋值符号中的值定义变量, 如果变量已经定义,赋值无效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x ?= new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo
y = foob
¶4) 追加赋值 +=
原变量值之后加上一个新值, 原变量值与新值之间由空格隔开
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x += $(y)
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo foob
y = foob
五、预定义变量的使用
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PHONY : all first second third
all : first second third
@echo “\$$@ => $@”
@echo “$$^ => $$^”
@echo “$$< => $<”
first :
second :
third :
输出结果:
$@ => all
$^ => first second third
$< => first
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $@ $^
main.o : main.c
$(CC) -o $@ -c $^
func.o : func.c
$(CC) -o $@ -c $^
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.PHONY : all out test first second third
all out : first second third
@echo "$(MAKE)"
@echo "$(MAKECMDGOALS)"
@echo "$(MAKEFILE_LIST)"
first :
@echo "first"
second :
@echo "second"
third :
@echo "third"
test :
@$(MAKE) first
@$(MAKE) second
@$(MAKE) third
运行 make 结果:
first
second
third
make
makefile
运行 make test 结果
make[1]: Entering directory '/home/book/c/mkfile'
first
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
second
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
third
make[1]: Leaving directory '/home/book/c/mkfile'
编程实验 2
1
2
3
4
5
6
7
8
9
10
11
.PHONY : test1 test2
test1 :
@echo "$(MAKE_VERSION)"
@echo "$(CURDIR)"
@echo "$(.VARIABLES)"
运行结果:
4.1
/home/book/c/mkfile
<D ?F .SHELLFLAGS CWEAVE ?D @D @F MAKE_VERSION CURDIR SHELL RM CO COMPILE.mod _ PREPROCESS.F LINK.m LINK.o OUTPUT_OPTION COMPILE.cpp MAKEFILE_LIST GNUMAKEFLAGS LINK.p XDG_DATA_DIRS DBUS_SESSION_BUS_ADDRESS CC CHECKOUT,v LESSOPEN CPP LINK.cc SSH_CONNECTION PATH LD TEXI2DVI YACC SSH_TTY XDG_RUNTIME_DIR ARFLAGS LINK.r LINT COMPILE.f LINT.c YACC.m YACC.y AR .FEATURES TANGLE LS_COLORS GET %F DISPLAY COMPILE.F CTANGLE .LIBPATTERNS LINK.C PWD LINK.S PREPROCESS.r *D LINK.c LINK.s HOME LESSCLOSE LOGNAME ^D MAKELEVEL COMPILE.m MAKE SHLVL AS PREPROCESS.S COMPILE.p XDG_SESSION_ID USER FC .DEFAULT_GOAL %D WEAVE MAKE_COMMAND LINK.cpp F77 OLDPWD .VARIABLES PC *F COMPILE.def LEX ARCH MAKEFLAGS MFLAGS SSH_CLIENT MAIL LEX.l LEX.m +D COMPILE.r MAKE_TERMOUT +F M2C CROSS_COMPILE MAKEFILES COMPILE.cc <F CXX COFLAGS COMPILE.C ^F COMPILE.S LINK.F SUFFIXES COMPILE.c COMPILE.s .INCLUDE_DIRS .RECIPEPREFIX MAKEINFO MAKE_TERMERR OBJC MAKE_HOST TEX LANG TERM F77FLAGS LINK.f
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、伪目标的引入
默认情况下,make 认为目标对应着一个文件,make 比较目标文件和依赖文件的新旧关系,决定是否执行命令,make 以文件处理作为第一优先级。但有时候我们希望我们的目录并不是一个文件如下所示:
1 | clean : |
这个时候通过 .PHONY 关键字声明一个伪目标, 伪目标不对应任何实际的文件,不管目标的依赖是否更新,命令总是执行, 伪目标的语法如下
1 | .PHONY : Target |
伪目标的本质是 make 中特殊目标 .PHONY 的依赖,先声明、后使用。
1 | .PHONY : clean rebuild all |
原理:当一个目标的依赖包含伪目标时,伪目标所定义的命令总是会被执行
¶2、绕开 .PHONY 关键字定义伪目标
如果一个规则没有命令或者依赖,并且它的目标不是一个存在的文件名;在执行此规则时,目标总是被认为是最新的。
1 | clean : FORCE |
四、变量和不同的赋值方式
¶1、makefile中的变量
makefile 中支持程序设计语言中变量的概念,makefile 中变量只代表文本数据。makefile 中变量命名规则如下所示
- 变量名大小写敏感
- 变量名可以包含字符,数字,下划线
- 不能包含 “:” , “#” , “=” , 或 " "
- makefile 未赋值的变量的值为空值
¶2、变量的定义和使用
修改前面的 makefile 验证
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $(TARGET) main.o func.o
main.o : main.c
$(CC) -o main.o -c main.c
func.o : func.c
$(CC) -o func.o -c func.c
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、makefile中变量的赋值方式
¶1) 简单赋值 :=
程序设计语言中的通用赋值方式, 只针对当前语句变量有效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x := new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = foob
¶2) 递归赋值 =
赋值操作可能影响多个其他变量, 所有与目标变量相关的其他变量将受到影响
1
2
3
4
5
6
7
8
9
10
11
12
x = foo
y = $(x)b
x = new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = new
y = newb
¶3) 条件赋值 ?=
如果变量未定义,使用赋值符号中的值定义变量, 如果变量已经定义,赋值无效
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x ?= new
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo
y = foob
¶4) 追加赋值 +=
原变量值之后加上一个新值, 原变量值与新值之间由空格隔开
1
2
3
4
5
6
7
8
9
10
11
12
x := foo
y := $(x)b
x += $(y)
.PHONY : test
test :
@echo “x = $(x)”
@echo “y = $(y)”
输出结果:
x = foo foob
y = foob
五、预定义变量的使用
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PHONY : all first second third
all : first second third
@echo “\$$@ => $@”
@echo “$$^ => $$^”
@echo “$$< => $<”
first :
second :
third :
输出结果:
$@ => all
$^ => first second third
$< => first
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $@ $^
main.o : main.c
$(CC) -o $@ -c $^
func.o : func.c
$(CC) -o $@ -c $^
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.PHONY : all out test first second third
all out : first second third
@echo "$(MAKE)"
@echo "$(MAKECMDGOALS)"
@echo "$(MAKEFILE_LIST)"
first :
@echo "first"
second :
@echo "second"
third :
@echo "third"
test :
@$(MAKE) first
@$(MAKE) second
@$(MAKE) third
运行 make 结果:
first
second
third
make
makefile
运行 make test 结果
make[1]: Entering directory '/home/book/c/mkfile'
first
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
second
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
third
make[1]: Leaving directory '/home/book/c/mkfile'
编程实验 2
1
2
3
4
5
6
7
8
9
10
11
.PHONY : test1 test2
test1 :
@echo "$(MAKE_VERSION)"
@echo "$(CURDIR)"
@echo "$(.VARIABLES)"
运行结果:
4.1
/home/book/c/mkfile
<D ?F .SHELLFLAGS CWEAVE ?D @D @F MAKE_VERSION CURDIR SHELL RM CO COMPILE.mod _ PREPROCESS.F LINK.m LINK.o OUTPUT_OPTION COMPILE.cpp MAKEFILE_LIST GNUMAKEFLAGS LINK.p XDG_DATA_DIRS DBUS_SESSION_BUS_ADDRESS CC CHECKOUT,v LESSOPEN CPP LINK.cc SSH_CONNECTION PATH LD TEXI2DVI YACC SSH_TTY XDG_RUNTIME_DIR ARFLAGS LINK.r LINT COMPILE.f LINT.c YACC.m YACC.y AR .FEATURES TANGLE LS_COLORS GET %F DISPLAY COMPILE.F CTANGLE .LIBPATTERNS LINK.C PWD LINK.S PREPROCESS.r *D LINK.c LINK.s HOME LESSCLOSE LOGNAME ^D MAKELEVEL COMPILE.m MAKE SHLVL AS PREPROCESS.S COMPILE.p XDG_SESSION_ID USER FC .DEFAULT_GOAL %D WEAVE MAKE_COMMAND LINK.cpp F77 OLDPWD .VARIABLES PC *F COMPILE.def LEX ARCH MAKEFLAGS MFLAGS SSH_CLIENT MAIL LEX.l LEX.m +D COMPILE.r MAKE_TERMOUT +F M2C CROSS_COMPILE MAKEFILES COMPILE.cc <F CXX COFLAGS COMPILE.C ^F COMPILE.S LINK.F SUFFIXES COMPILE.c COMPILE.s .INCLUDE_DIRS .RECIPEPREFIX MAKEINFO MAKE_TERMERR OBJC MAKE_HOST TEX LANG TERM F77FLAGS LINK.f
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、makefile中的变量
makefile 中支持程序设计语言中变量的概念,makefile 中变量只代表文本数据。makefile 中变量命名规则如下所示
- 变量名大小写敏感
- 变量名可以包含字符,数字,下划线
- 不能包含 “:” , “#” , “=” , 或 " "
- makefile 未赋值的变量的值为空值
¶2、变量的定义和使用
修改前面的 makefile 验证
1 | CC := gcc |
¶3、makefile中变量的赋值方式
¶1) 简单赋值 :=
程序设计语言中的通用赋值方式, 只针对当前语句变量有效
1 | x := foo |
¶2) 递归赋值 =
赋值操作可能影响多个其他变量, 所有与目标变量相关的其他变量将受到影响
1 | x = foo |
¶3) 条件赋值 ?=
如果变量未定义,使用赋值符号中的值定义变量, 如果变量已经定义,赋值无效
1 | x := foo |
¶4) 追加赋值 +=
原变量值之后加上一个新值, 原变量值与新值之间由空格隔开
1 | x := foo |
五、预定义变量的使用
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PHONY : all first second third
all : first second third
@echo “\$$@ => $@”
@echo “$$^ => $$^”
@echo “$$< => $<”
first :
second :
third :
输出结果:
$@ => all
$^ => first second third
$< => first
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CC := gcc
TARGET := hello.out
$(TARGET) : main.o func.o
$(CC) -o $@ $^
main.o : main.c
$(CC) -o $@ -c $^
func.o : func.c
$(CC) -o $@ -c $^
.PHONY : rebuild clean all
rebuile : clean all
all : $(TARGET)
clean :
rm *.o $(TARGET)
运行结果:
gcc -o main.o -c main.c
gcc -o func.o -c func.c
gcc -o hello.out main.o func.o
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
.PHONY : all out test first second third
all out : first second third
@echo "$(MAKE)"
@echo "$(MAKECMDGOALS)"
@echo "$(MAKEFILE_LIST)"
first :
@echo "first"
second :
@echo "second"
third :
@echo "third"
test :
@$(MAKE) first
@$(MAKE) second
@$(MAKE) third
运行 make 结果:
first
second
third
make
makefile
运行 make test 结果
make[1]: Entering directory '/home/book/c/mkfile'
first
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
second
make[1]: Leaving directory '/home/book/c/mkfile'
make[1]: Entering directory '/home/book/c/mkfile'
third
make[1]: Leaving directory '/home/book/c/mkfile'
编程实验 2
1
2
3
4
5
6
7
8
9
10
11
.PHONY : test1 test2
test1 :
@echo "$(MAKE_VERSION)"
@echo "$(CURDIR)"
@echo "$(.VARIABLES)"
运行结果:
4.1
/home/book/c/mkfile
<D ?F .SHELLFLAGS CWEAVE ?D @D @F MAKE_VERSION CURDIR SHELL RM CO COMPILE.mod _ PREPROCESS.F LINK.m LINK.o OUTPUT_OPTION COMPILE.cpp MAKEFILE_LIST GNUMAKEFLAGS LINK.p XDG_DATA_DIRS DBUS_SESSION_BUS_ADDRESS CC CHECKOUT,v LESSOPEN CPP LINK.cc SSH_CONNECTION PATH LD TEXI2DVI YACC SSH_TTY XDG_RUNTIME_DIR ARFLAGS LINK.r LINT COMPILE.f LINT.c YACC.m YACC.y AR .FEATURES TANGLE LS_COLORS GET %F DISPLAY COMPILE.F CTANGLE .LIBPATTERNS LINK.C PWD LINK.S PREPROCESS.r *D LINK.c LINK.s HOME LESSCLOSE LOGNAME ^D MAKELEVEL COMPILE.m MAKE SHLVL AS PREPROCESS.S COMPILE.p XDG_SESSION_ID USER FC .DEFAULT_GOAL %D WEAVE MAKE_COMMAND LINK.cpp F77 OLDPWD .VARIABLES PC *F COMPILE.def LEX ARCH MAKEFLAGS MFLAGS SSH_CLIENT MAIL LEX.l LEX.m +D COMPILE.r MAKE_TERMOUT +F M2C CROSS_COMPILE MAKEFILES COMPILE.cc <F CXX COFLAGS COMPILE.C ^F COMPILE.S LINK.F SUFFIXES COMPILE.c COMPILE.s .INCLUDE_DIRS .RECIPEPREFIX MAKEINFO MAKE_TERMERR OBJC MAKE_HOST TEX LANG TERM F77FLAGS LINK.f
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
在 makefile 中存在一些预定义的变量,常用的有自动变量以及特殊变量
¶1. 自动变量
$@ 当前规则中触发命令被执行的目标, 即当前规则中的目标
$^ 当前规则中的所有依赖
$< 当前规则中的第一个依赖
¶2、自动变量的使用
1 | PHONY : all first second third |
小贴士:
“$” 对于 makefile 有特殊含义,输出时加上一个 $ 进行转义
“$@” 对于Bash Shell 有特殊含义,输出时加上 \ 进行转义
使用自动变量改写前面的 makefile
1 | CC := gcc |
¶3、特殊变量
-
$(MAKE): 当前 make 解释器的文件名
-
$(MAKECMDGOALS): 命令中指定的目标名( make 的命令行参数)
-
$(MAKEFILE_LIST): make 所需要处理的 makefile 文件列表, 当前 makefile 的文件名总是位于列表的最后, 文件名之间以空格进行划分
-
$(MAKE_VERSION): 当前 make 解释器的版本
-
$(CURDIR): 当前 make 解释器的工作目录
-
$(.VARIABLES): 所有已经定义的变量名列表,其中包括预定义变量和自定义变量
编程实验 1
1 | .PHONY : all out test first second third |
编程实验 2
1 | .PHONY : test1 test2 |
六、变量的高级主题
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1
$(var:a=b) 或 ${var:a=b}
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
src := a.cc b.cc c.cc
obj := $(src:cc=o)
test :
@echo ”obj => $(obj)”
输出结果:obj = > a.o b.o c.o
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1
$(var:a%b=x%y)或${var:a%b=x%y}
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1
2
3
4
5
6
7
8
9
10
11
src := a1b.c a2b.c a3b.c
obj := $(src:a%b.c=x%y)
.PHONY : test
test :
@echo "obj => $(obj)"
输出结果:
obj => x1y x2y x3y
¶3)规则中的模式替换
1
2
3
4
targets : targets-pattern : prereq-pattern
command1
command2
…
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
上述 makefile 等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
mian.o : main.c
gcc -o mian.o -c mian.c
func.o : func.c
gcc -o func.o -c func.o
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
除此之外呢,我们还可以省略掉 targets 如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.PHONY : rebuild clean all
CC := gcc
TARGET := hello.out
OBJS := main.o func.o
$(TARGET) : $(OBJS)
$(CC) -o $@ $^
##############################
%.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
#############################
rebuild : clean all
all : $(TARGET)
clean :
rm -rf *.o $(TARGET)
示例代码在大部分情况可以理解为等价的,特殊情况如下
1
2
3
4
5
%.o : %.c
$(CC) -o $@ -c $^
$(OBJS) : %.o : %.c // 规则中的模式替换
$(CC) -o $@ -c $^
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1
2
3
4
5
6
7
8
9
x := y
y := z
a := $($(x))
test :
@echo "a ==> $(a)"
运行结果:
a ==> z
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1
2
3
4
5
6
7
hm := hello makefile
test :
@echo “hm => $(hm)”
执行:make hm := cmd
运行结果:hm => cmd
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1
2
3
4
5
6
7
override var := test
test :
@echo “var => $(var)”
执行:make var:=cmd
执行结果:hm => test
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
define foo
I’m fool:
endef
override define cmd
@echo "run cmd begin ..."
@echo "run cmd end ..."
endef
test :
@echo "$(foo)"
$(cmd)
运行结果:
I’m fool:
run cmd begin ...
run cmd end ...
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1
2
3
4
5
6
7
8
9
10
11
PWD := pwd # 修改系统环境变量
export var := D.T.Software # 定义临时环境变量
version := v1 # 普通变量
user :=baron # 普通变量
test :
@echo "PWD ==> $(PWD)"
@echo "make another file ..."
$(MAKE) -f makefile2
$(MAKE) -f makefile2 user:=$(user) # 将 user 这个变量通过 make 进行传递
makefile2 文件如下
1
2
3
4
5
test :
@echo "PWD ==> $(PWD)"
@echo "var = $(var)"
@echo "version = $(version)"
@echo "user = $(user)"
运行结果如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PWD ==> pwd
make another file ...
make -f makefile2
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user =
make[1]: Leaving directory '/home/book/c/mkfile'
make -f makefile2 user:=baron
make[1]: Entering directory '/home/book/c/mkfile'
PWD ==> pwd
var = D.T.Software
version =
user = baron
make[1]: Leaving directory '/home/book/c/mkfile'
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1
target : variable_name := variable-value
举例说明
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
var := D.T.Software
test : var := test-var
test : test1
@echo "test:"
@echo "var => $(var)"
test1 :
@echo "test1:"
@echo "var ==> $(var)"
test2 :
@echo "test2:"
@echo "var ==> $(var)"
运行结果:
book@100ask:~/c/mkfile$ make
test1
var ==> test-var
test:
var => test-var
book@100ask:~/c/mkfile$
book@100ask:~/c/mkfile$ make test2
test2:
var ==> D.T.Software
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1
%target : variable_name := variable-value
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1
2
3
4
5
6
7
8
9
new := TDelphi
%e : override new := test-new
rule :
@echo "rule:"
@echo "new => $(new)"
运行结果:
rule:
new => test-new
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1. 常用语法
¶1) 变量值的替换
使用指定字符(串)替换变量中的后缀字符(串),语法格式如下
1 | $(var:a=b) 或 ${var:a=b} |
替换表达式中不能有任何的空格,make 中支持使用 ${} 对变量进行取值
1 | src := a.cc b.cc c.cc |
¶2)变量的模式替换
使用 % 保留变量值中的指定字符,替换其他字符,语法格式如下
1 | $(var:a%b=x%y)或${var:a%b=x%y} |
替换表达式中不能有任何的空格, make 中支持使用 ${} 对变量进行取值
1 | src := a1b.c a2b.c a3b.c |
¶3)规则中的模式替换
1 | targets : targets-pattern : prereq-pattern |
当 targets(目标) 的依赖的规则不存在时,通过 targets-pattern 从 targets 中匹配子目标,再通过 prereq-pattern 从子目标生成依赖, 进而构成完整的规则, 举例说明如下。
1 | .PHONY : rebuild clean all |
上述 makefile 等价于
1 | .PHONY : rebuild clean all |
除此之外呢,我们还可以省略掉 targets 如下所示
1 | .PHONY : rebuild clean all |
示例代码在大部分情况可以理解为等价的,特殊情况如下
1 | %.o : %.c |
当他们同时存在的时候,$(CC) -o $@ -c $^ 代码会被执行两次。并且 $(OBJS) : %.o : %.c 的规则会被替换为 %.o : %.c 的规则
¶4)变量值的嵌套引用
一个变量名之中可以包含对其他变量的引用,嵌套引用的本质是使用一个变量表示另一个变量
1 | x := y |
¶5)命令行变量
运行 make 时,在命令行定义变量,命令行变量默认覆盖 makefile 中定义的变量
1 | hm := hello makefile |
¶6)override 关键字
用于指示 makefile 中定义的变量不能被覆盖, 变量的定义和赋值都要用到 override 关键字
1 | override var := test |
¶7)define关键字
用于在 makefile 中定义多行变量, 多行变量的定义从变量名开始到 endef 结束, 可以使用 override 关键字防止变量被覆盖, define 定义的变量等价于使用 = 定义的变量
1 | define foo |
¶2、环境变量(全局变量)
makefile 中能够直接使用环境变量的值,定义了同名变量,默认环境变量将被覆盖。运行 make 时指定 “-e” 选项使用系统默认环境变量。环境变量可以在所有的 makefile 中使用,过多的依赖环境变量会使系统的移植性降低。
变量在不同的 makefile 中的传递方式有三种:直接在外部定义环境变量进行传递、使用 export 定义变量进行传递(定义临时环境变量)、定义 make 命令行变量进行传递(推荐)。
编程实验, makefile 文件如下
1 | PWD := pwd # 修改系统环境变量 |
makefile2 文件如下
1 | test : |
运行结果如下
1 | PWD ==> pwd |
¶3、目标变量(局部变量)
作用域只在指定目标及连带规则中
语法格式
1 | target : variable_name := variable-value |
举例说明
1 | var := D.T.Software |
¶4、模式变量(局部变量)
模式变量是目标变量的扩展,作用域只在符合模式的目标及连带规则中
语法格式
1 | %target : variable_name := variable-value |
举例说明
当前要定义一个名为 new 的局部变量,new 的作用域是所有已 e 结尾的目标及连带规则。
1 | new := TDelphi |
七、条件判断语句
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1
2
3
4
5
ifxxx (arg1,arg2) #注意括号里面不能使用空格
# for true
else
# for false
endif
其他合法形式
1
2
3
4
ifxxx "arg1" "arg2"
ifxxx "arg1" 'arg2'
ifxxx 'arg1' "arg2"
ifxxx 'arg1' "arg2'
¶2、条件判断关键字
关键字
功能
ifeq ($(x),$(y))
判断参数是否相等,相等为 true ,否则为 false
ifneq ($(x),$(y))
判断参数是否不相等,不相等则为 true,否则为false
ifdef x
判断变量是否有值,有值为 true,否则为false
ifndef x
判断变量是否没有值,没有为 true,否则为false
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : test
var1 := cmd
var2 := $(var1)
test:
ifeq ($(var1),$(var2)) # 注意最前面不是 table 隔开是 4 个空格
@echo "var1 == var2"
else
@echo "var1 != var2"
endif
ifneq ($(var1),$(var2))
@echo "var1 != var2"
else
@echo "var1 == var2"
endif
ifdef var1
@echo "var1 is NOT empty"
else
@echo "var1 is empty"
endif
ifndef var1
@echo "var1 is empty"
else
@echo "var1 is NOT empty"
endif
运行结果:
var1 == var2
var1 == var2
var1 is NOT empty
var1 is NOT empty
¶2)条件判断关键字异常分析
¶a)异常情况1
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is NOT defined
var4 is defined
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.PHONY : test
var3 =
var4 = $(var3)
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
var3 = 3
运行结果:
var3 is NOT defined
var4 is defined
¶c)异常情况3
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.PHONY : test
var3 =
var4 = $(var3)
var3 = 3
test:
ifdef var3
@echo "var3 is defined"
else
@echo "var3 is NOT defined"
endif
ifdef var4
@echo "var4 is defined"
else
@echo "var4 is NOT defined"
endif
运行结果:
var3 is defined
var4 is defined
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、makefile中的条件判断语句
可以根据条件值来决定 make 的执行、可以比较两个不同变量或变量和常量值。基本形式如下:
1 | ifxxx (arg1,arg2) #注意括号里面不能使用空格 |
其他合法形式
1 | ifxxx "arg1" "arg2" |
¶2、条件判断关键字
关键字 | 功能 |
---|---|
ifeq ($(x),$(y)) | 判断参数是否相等,相等为 true ,否则为 false |
ifneq ($(x),$(y)) | 判断参数是否不相等,不相等则为 true,否则为false |
ifdef x | 判断变量是否有值,有值为 true,否则为false |
ifndef x | 判断变量是否没有值,没有为 true,否则为false |
注意:条件判断语句只能用与控制 make 中实际执行的语句;但是,不能控制规则中命令的执行过程。
¶3、工程使用小结
- 条件判断语句之前可以有空格,但不能有tab字符 ('t')
- 在条件判断语句中不要用自动变量 ($@,$^,$<)
- 一条完整的条件语句必须位于同一个 makefile 中
- 条件判断类似于 c 语言中的宏,预处理阶段有效,执行阶段无效
- make 在加载 makefile 时首先计算表达式的值(赋值方式不同计算方式不同),根据判断语句的表达式决定执行的内容
¶5、举例说明
¶1)条件判断关键字程序实例
1 | .PHONY : test |
¶2)条件判断关键字异常分析
¶a)异常情况1
1 | .PHONY : test |
运行结果分析
我们所期望的运行结果是 var3 与 var4 都是未定义,但这里运行结果表示 var4 的值为定义。
原因分析
产生这个结果的原因就是,make 在加载 makefile 时首先计算表达式的值,由于我们使用的赋值表达式为 = 因此,makfile 在预处理 ifdef var4 时无法确定 var4 的值是否定义,因此 make 解释器在这里默认为 var4 的值为已定义的值。
¶b)异常情况2
1 | .PHONY : test |
¶c)异常情况3
1 | .PHONY : test |
小贴士:a为不对 var3 赋值, b、c 为 var 赋值的位置不同产生的结果不同
八、函数的定义以及调用
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1
2
3
4
5
6
7
8
define func1
@echo “My name is $(0)”
endef
define func2
@echo “My name is $(0)”
@echo “Param => $(1)”
endef
函数调用:
1
2
3
test :
$(call func1)
$(call func2,D.T.Software)
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
$(func1)
$(func2)
@echo ""
$(call func1)
$(call func2, Delphi)
运行结果:
My name is
My name is
Param =>
My name is func1
My name is func2
Param => Delphi
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
define func1
@echo "My name is $(0)"
endef
define func2
@echo "My name is $(0)"
@echo "Param => $(1)"
endef
.PHONY : test
test :
@echo "My name is "
@echo "My name is "
@echo "Param => "
@echo ""
@echo "My name is func1"
@echo "My name is func2"
@echo "Param => Delphi"
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、makefile 支持函数的概念
make 解释器提供了一系列的函数供 makefile 调用。在 makefile 中支持自定义函数实现,并调用执行。通过 define 关键字实现自定义函数
¶2、自定义函数语法
1 | define func1 |
函数调用:
1 | test : |
函数通过call关键字调用,$(0)、$(1)、$(3)、…代表依次传入的参数
¶3、深入理解自定义函数
自定义函数是一个多行变量,无法直接调用。自定义函数是一个过程调用,没有任何返回值。自定义函数用于定义命令集合,并应用于规则中
¶4. 实例分析
1 | define func1 |
函数调用的本质就是宏替换的过程,通过 call 可以将相应的 “形参” 替换成 “实参” $(0)、$(1)、$(3)、…,因此上述代码在运行的时候等价于
1 | define func1 |
九、变量与函数的综合示例
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1
$(wildcard _pattern)
获取当前工作目录中满足 _pattern 的文件或目录列表
1
$(addprefix _prefix, _names)
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1
SRCS := $(wildcard *.c)
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1
OBJS := $(SRCS:.c=.o)
- 3) 对每个目标文件加上前缀路径(函数调用)
1
$(addprefix apth/,$(OBJS))
¶4、编译规则的依赖
¶5. 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
CC := gcc
RM := rm -rf
MKDIR := mkdir
DIR_OBJS := objs
DIR_TARGETS := target
DIR := $(DIR_OBJS) $(DIR_TARGETS)
TARGET := $(DIR_TARGETS)/hello.out
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
.PHONY := rebuild clean all
$(TARGET) : $(DIR) $(OBJS)
$(CC) -o $@ $(OBJS)
@echo "Target File ==> $@"
$(DIR) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
ifeq ($(DEBUG),true)
$(CC) -o $@ -g -c $^
else
$(CC) -o $@ -c $^
endif
clean :
$(RM) $(DIR)
rebuild : clean all
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、实战需求
自动生成 target 文件夹存放可执行文件, 自动生成 bojs 文件夹存放编译生成的目标文件(*.o),支持调试版本的编译选项,考虑代码的扩展性
¶2、工具原料
1 | $(wildcard _pattern) |
获取当前工作目录中满足 _pattern 的文件或目录列表
1 | $(addprefix _prefix, _names) |
给名字列表 _name 中的每一个名字增加前缀 _prefix
¶3、关键技巧
- 1) 自动获取当前目录小的文件列表(函数调用)
1 | SRCS := $(wildcard *.c) |
- 2) 根据源文件列表生成目标文件列表(变量的值替换)
1 | OBJS := $(SRCS:.c=.o) |
- 3) 对每个目标文件加上前缀路径(函数调用)
1 | $(addprefix apth/,$(OBJS)) |
¶4、编译规则的依赖
¶5. 编程实现
1 | CC := gcc |
在当前目录下创建 objs target 文件,编译链接当前目录下 .c 文件,将生成的 .o 文件存放到 objs 文件夹,生成的可执行文件 app.out 存放在 target 文件夹,最后运行可执行程序 app.out 。其中当 DEBUG=true 时添加编译选项 -g 实现调试功能
十、include 关键字
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1
2
test :
@echo "this is test"
makefile 内容
1
2
3
4
5
6
7
8
9
.PHONY : all clean
include test.txt
all :
echo "this is all"
make all 运行结果:
this is test
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
9
10
11
12
include test.txt
all :
@echo "this is all"
test.txt :
@echo "test.txt is not exist"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
test.txt is not exist
this is all
¶b)规则不存在
test.txt 文件不存在
1
2
3
4
5
6
7
8
include test.txt
all :
@echo "this is all"
make all 运行结果:
makefile:2: test.txt: 没有那个文件或目录
make: *** 没有规则可以创建目标“test.txt”。 停止。
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1. include 关键字的处理方式
在当前目录搜索或指定目录搜索目标文件
¶1)搜索成功
类似于 c 语言中 include 关键字,将文件内容直接搬到 makefile 中, 编程实验如下
test.txt 文件中的内容
1 | test : |
makefile 内容
1 | .PHONY : all clean |
¶2)搜索失败
产生警告,以文件作为目标查找并执行相应规则,当文件名对应的规则不存在时,最终产生错误
¶a)规则存在
test.txt 文件不存在
1 | include test.txt |
¶b)规则不存在
test.txt 文件不存在
1 | include test.txt |
¶2. include关键字使用总结
¶1)当目标文件不存在
¶2)当目标文件存在
¶3) 减号 - 的使用
使用减号 - 不但关闭了 include 发出的警告,同时关闭了错误;当错误发生时 make 将忽略这些错误。
十一、自动生成依赖关系
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1
2
3
4
5
6
7
.PHONY : test
test :
@echo "test = > abc+abc=abc" | sed 's:abc:xyz:g'
运行结果:
test => xyz+xyz=xyz
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1
sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g'
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1
gcc -M test.c
获取目标的部分依赖关系,即用 "" 包含的头文件
1
gcc -MM test.c
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1
2
3
4
5
6
.PHONY : all
all :
mkdir test
cd test
mkdir subtest
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest
-
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。
-
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。
-
代码修改
1
2
3
4
5
6
7
.PHONY : all
all :
set -e; \
mkdir test; \
cd test; \
mkdir subtest
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
CC := gcc
RM := rm -rf
MKDIR := mkdir
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
-include $(DEPS)
all :
@echo all
%.dep : %.c
@echo "Creating $@..."
set -e;\
$(CC) -MM $^ | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean:
$(RM) *.dep
运行结果:
Creating func.dep ...
Creating main.dep ...
make: Nothing to be done for 'objs/main.o'.
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
-include $(DEPS)
all :
@echo "all"
$(DIR_DEPS):
$(MKDIR) $@
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS)
运行结果:
mkdir deps
Creating deps/sub.dep ...
Creating deps/add.dep ...
Creating deps/a.dep ...
Creating deps/sub.dep ...
all
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1
2
3
4
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
¶4) 自动生成依赖的最终实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
.PHONY : all clean
MKDIR := mkdir
RM := rm -fr
CC := gcc
DIR_DEPS := deps
DIR_OBJS := objs
DIR_EXE := exes
DIRS := $(DIR_OBJS) $(DIR_EXE)
APP = $(DIR_EXE)/app.out
SRCS := $(wildcard *.c)
DEPS := $(SRCS:.c=.dep)
DEPS := $(addprefix $(DIR_DEPS)/,$(DEPS))
OBJS := $(SRCS:.c=.o)
OBJS := $(addprefix $(DIR_OBJS)/,$(OBJS))
all : $(APP)
./$(APP)
$(APP) : $(DIRS) $(OBJS)
@echo "target ==> $@"
$(CC) -o $@ $(OBJS)
$(DIRS) :
$(MKDIR) $@
$(DIR_OBJS)/%.o : %.c
$(CC) -c $(filter %.c, $^) -o $@
ifeq ("$(MAKECMDGOALS)","all")
-include $(DEPS)
endif
ifeq ("$(MAKECMDGOALS)","")
-include $(DEPS)
endif
$(DIR_DEPS):
$(MKDIR) $@
ifeq ($(wildcard $(DIR_DEPS)), )
$(DIR_DEPS)/%.dep : $(DIR_DEPS) %.c
else
$(DIR_DEPS)/%.dep : %.c
endif
@echo "Creating $@ ..."
@set -e; \
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' > $@
clean :
$(RM) $(DIR_DEPS) $(DIRS)
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1
$(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、编译行为带来的缺陷
预处理器直接将头文件中的代码直接插入源文件,编译器只通过预处理后的源文件产生目标文件,因此,规则中以源文件为依赖,修改头文件后,命令可能无法执行
¶2.、解决方案
可以将头文件作为依赖条件出现于每一个目标对应的规则中,当头文件改动,任何源文件都将被重新编译(编译低效),但是这种方案当项目中头文件数量巨大时,makefile 将很难维护
由于上述方案存在的缺陷,思考是否可以自动生成依赖解决,通过命令自动生成对头文件的依赖,将生成的依赖自动包含进 makefile 中,当头文件改动后,自动确认需要重新编译的文件。
¶3、预备知识
自动生成依赖文件需要一些预备知识
¶1) sed 流编辑器
sed 是一个流编辑器,用于流文本的修改(增/删/查/改),sed 可以用于流文本中的字符串替换,sed 字符串替换方式为:sed ‘s:src:des:g’。sed 字符串替换实例如下:
1 | .PHONY : test |
sed 还支持正则表达式,在 sed 中可以用正则表达式匹配替换目标,并且可以使用匹配的目标生成替换结果
1 | sed 's,\(.*\)\.o[ :]*,objs/\1.o : ,g' |
该表达式的结果为将流文本中的.o文件加上路径前缀 objs/
¶2) gcc 关键编译选项
获取目标的完整依赖关系,即包含 “” 和 <> 两者的头文件
1 | gcc -M test.c |
获取目标的部分依赖关系,即用 "" 包含的头文件
1 | gcc -MM test.c |
¶3) 拆分目标的依赖
将目标的完整依赖拆分为多个部分依赖
¶4) make 中的命令执行机制
规则中的每个命令默认是在一个新的进程中执行(shell)
1 | .PHONY : all |
-
运行结果
期待的运行结果为,创建一个文件夹 test,在 test 中创建子文件夹 subtest,但实际运行结果为在当前目录下创建出两个文件夹分别为 test 和 subtest -
运行结果分析
make 创建一个进程执行 mkdir test,执行完后进程结束,回到当前目录。make 又创建一个进程执行 cd test,执行完后进程结束,回到当前目录。make 再次创建一个进程执行 mkdir subtest,执行完后进程结束,回到当前目录。因此最终结果在当前目录下创建两个文件夹 test 和 subtest。 -
解决方案
set-e 指定发生错误后立即退出执行。可以通过分号 ; 和接续符 \ 将多个命令组合成一个命令。组合的命令一次在同一个进程中被执行,无论是否出错。 -
代码修改
1 | .PHONY : all |
¶4、自动生成依赖关系
¶1)自动生成依赖文件
通过 include 指令包含所有的 .dep 依赖文件,当 .dep 依赖文件不存在时,使用规则自动生成
1 | .PHONY : all clean |
¶2) 集中管理 .dep 文件
当 include 发现 .dep 文件不存在,通过规则和命令创建 deps 文件夹,将所有的 .dep 文件创建到 deps 文件夹,.dep 文件中记录目标文件的依赖关系
1 | .PHONY : all clean |
¶3) 一些 .dep 文件会被重复创建多次
deps 文件夹的时间属性会因为依赖文件创建而发生改变,make 发现 deps 文件夹比对应的目标更新,触发相应规则的重新解析和命令的执行
解决方案,添加宏代码块进行判断,当不存在 deps 文件添加文件依赖关系,否则取消文件依赖关系
1 | ifeq ($(wildcard $(DIR_DEPS)), ) |
¶4) 自动生成依赖的最终实现
1 | .PHONY : all clean |
代码存在问题,当头文件中包含其他头文件时可能出现修改头文件,make 无法执行的情况。解决方案,将依赖文件名,作为目标加入自动生成的依赖关系中,通过 include 加载依赖文件时判断是否执行规则,在规则执行时重新生成依赖关系文件,最后加载新的依赖文件。即修改上述代码 52 行,在冒号前加入 $@
1 | $(CC) -MM -E $(filter %.c, $^) | sed 's,\(.*\)\.o[ :]*,objs/\1.o $@: ,g' > $@ |
十二、make 的隐式规则
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
SRCS := $(wildcard *.c)
OBJS := $(SRCS:.c=.o)
app.out : $(OBJS)
$(CC) -o $@ $^
$(RM) $^
@echo "Target ==> $@"
运行结果:
cc -c -o main.o main.c
cc -c -o func.o func.c
cc -o app.out main.o func.o
rm -f main.o func.o
Target ==> app.out
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1
make -p
查看具体规则
1
make -p | grep "XXX"
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1
.cpp.o <==> %.o : %.cpp
单后缀规则,定义单个后缀文件(源文件后缀)
1
.c <==> % : %.c
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
0%
¶1、makfile 中出现同名目标时
所有的依赖合并在一起,成为目标的最终依赖。当多处出现同一目标命令时,make 发出警告。所有之前定义的命令被最后定义的命令取代.
注意事项:
当使用 include 关键字包含其他文件时,需要确保被包含文件中的同名目标只有依赖,没有命令否则,同名文件将被覆盖!
¶2、隐式规则
make 提供了一些常用的,预定义的规则实现。make 提供了生成目标文件的隐式规则,隐式规则会使用预定义变量完成编译工作,改变预定义变量将部分改变隐式规则的行为,当存在自定义规则时,不在使用隐式规则,当相应目标的规则未提供时,make 尝试使用隐式规则。
1 | SRCS := $(wildcard *.c) |
上述 makefile 没有 $(OBJS) 的规则,为什么能正常运行,就是因为 make 的标准库中提供了对应的规则,当 make 发现目标的依赖不存在时,尝试通过依赖名逐一查找隐式规则,并且通过依赖名推到可能需要的源文件。最后根据文件后缀自动推导编译命令。
¶3、隐式规则的副作用
编译行为难以控制,大量使用隐式规则可能产生意想不到的编译行为。编译效率低下,make 从隐式规则和自定义规则中选择最终使用的规则。隐式规则链,当依赖目标存在时,make 会极力组合各种隐式规则对目标进行创建,进而产生意料之外的编译行为!查看隐式规则的方式如下。
查看所有规则
1 | make -p |
查看具体规则
1 | make -p | grep "XXX" |
除此之外还可以使用前面提到的 .VARIABLES 预定义变量查看 make 的自定义变量。我们可以通过下面方式禁用隐式规则。在 makefile 中自定义规则,在 makefile 中自定义模式(如:%.o : %.c),全局禁用:make -r。
¶4、后缀规则
后缀规则是旧式的“模式规则”,可以通过后缀描述的方式自定义规则
双后缀规则,定义一对文件后缀(依赖文件后缀和目标文件后缀)
1 | .cpp.o <==> %.o : %.cpp |
单后缀规则,定义单个后缀文件(源文件后缀)
1 | .c <==> % : %.c |
后缀规则需要注意,后缀规则中不允许有依赖。后缀规则必须有命令,否则无意义。后缀规则将逐步被模式规则取代。
十三、makefile 中的路径搜索
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Project
|
|
|--------->Module1
| |
| |--------->inc
| |
| |--------->src
|
|--------->Module2
|
|--------->inc
|
|--------->src
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1
2
3
VPATH := inc src (空格)
VPATH := inc;src (分号)
VPATH := inc:src (冒号)
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
APP := app.out
DIR_INC := inc
DIR_SRC := src
VPATH := inc src
CFLAGS := -I$(DIR_INC)
all : $(APP)
@echo "target ==> $(APP)"
$(APP) : a.o sub.o add.o
$(CC) -o $@ $^
%.o : %.c
$(CC) $(CFLAGS) -c $^ -o $@
clean :
$(RM) *.o *.out
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1
vpath Pattern Directory
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1
2
vpath %.h inc
vpath %.c src
¶2、取消搜索规则
取消已经设置的某个搜索规则
1
vpath Pattern
示例
1
2
vpath %.h inc # 在 inc 中搜索 .h 文件
Vpath %h # 不在 inc 中搜索 .h 文件
取消所有已经设置的规则
1
vpath
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹
-
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1
$(wildcard $(DIR)/_pattern)
去除 _names 中每一个文件名的路径前缀
1
$(notdir _names)
将 _text 中符合 _pattern 的部分替换为 replacement
1
$(patsubst _pattern, replacement, _text)
¶3) 关键技巧
自动获取源文件列表(函数调用)
1
SRCS := $(wildcard src/*.c)
根据源文件列表生成目标文件列表(变量值的替换)
1
OBJS := $(SRCS:.c=.o)
替换每一个目标的路径前缀(函数调用)
1
OBJS := $(patsubst src/%,build/%,$(OBJS))
¶4) 编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
.PHONY : all clean
MKDIR := mkdir
RM := rm -rf
CC := gcc
DIR_INC := inc
DIR_SRC := src
DIR_BUILDS := build
TYPE_INT := .h
TYPE_SRC := .c
TYPE_OBJ := .o
DEBUG :=
LFLAGS :=
CFLAGS := -I $(DIR_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
APP := $(DIR_BUILDS)/app.out
SCRS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SCRS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_BUILDS)/%,$(OBJS))
HDRS := $(wildcard $(DIR_INC)/*$(TYPE_INC))
HDRS := $(notdir $(HDRS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
all : $(DIR_BUILDS) $(APP)
./$(APP)
$(DIR_BUILDS) :
$(MKDIR) $@
$(APP) : $(OBJS)
@echo "target ==> $(APP)"
$(CC) $(LFLAGS) -o $@ $^
$(DIR_BUILDS)/%$(TYPE_OBJ) : %$(TYPE_SRC) $(HDRS)
$(CC) $(CFLAGS) -c $< -o $@
clean :
$(RM) $(DIR_BUILDS)
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
项目中的 makefile 必须能够正确的定位源文件和依赖文件,最终编译生成可执行程序。常用的源代码管理方式如下
1 | Project |
¶1、特殊的预定义变量 VPATH(全大写)
VPATH 变量的值用于指示 make 如何查找文件,不同文件夹可作为 VPATH 的值同时出现,文件夹的名字需要使用分隔符进行区分
1 | VPATH := inc src (空格) |
¶1)make 对于 VAPATH 值得处理方式
当前文件夹找不到需要的文件时,VPATH会被使用,make 会在 VPATH 指定的文件夹中依次搜索文件,当多个文件夹存在同名文件时,选择第一次搜索到的文件
注意事项:
VPATH 只能决定 make 的搜索路径,无法决定命令的搜索路径
对于特定的编译命令(gcc),需要独立指定编译搜索路径
语法:gcc -I include-path
¶2)VAPATH 使用实例
.c 文件存放于 src 文件夹,.o 文件存放于 inc 文件夹,make 自动搜索相关路径生成可执行文件 app.out
1 | .PHONY : all clean |
由于当多个文件夹存在同名文件时,选择第一次搜索到的文件这也会带来问题, 当 inc 文件意外出现源文件(c/cpp文件),那可能出现编译错误!可以使用 vpath 为不同类型的文件指定不同的搜索路径。
1 | vpath Pattern Directory |
在 Directory 下搜索符合 Pattern 的规则文件,将上述 VPATH 修改为 vpath
1 | vpath %.h inc |
¶2、取消搜索规则
取消已经设置的某个搜索规则
1 | vpath Pattern |
示例
1 | vpath %.h inc # 在 inc 中搜索 .h 文件 |
取消所有已经设置的规则
1 | vpath |
¶3、问题分析
-
当 VPATH 和 vpath 关键字同时出现,make 会如何处理?
make 首先在当前文件夹下搜索需要的文件,如果失败: make首先在 vpath 提供的路径下寻搜索目标文件。当搜索失败时,转而搜索 VPATH 指定的文件夹 -
当使用 vpath 对同一个 Pattern 指定多个文件夹时,make 会如何处理?
make 首先在当前文件夹搜索需要的文件,如果失败:make 以自上而下的顺序搜索 vpath 指定的文件夹。当找到目标文件,搜索结束
小贴士:
在实际工程开发中优先选择 vpath 关键字预防 make 隐式规则的副作用
- 通过 VPATH 变量指定搜索路径后,make 如何决定目标文件的最终位置?
当 app.out 完全不存在,make 在当前文件夹下创建 app.out。当 src 文件中存在 app.out,所有目标依赖的新旧关系不变,make 不会创建 app.out。当依赖文件被更新,make 在当前文件夹下创建 app.out
解决方案
使用 GPATH 特殊变量指定目标文件夹 GPATH := src
当 app.out 完全不存在,make 默认在当前文件夹创建 app.out。当 app.out 存在于 src,且依赖文件被更新。make 在 src 中创建 app.out
¶4、工程建议
尽量使用 vpath 为不同文件指定搜索路径、不要在源代码文件夹中生成目标文件、为编译得到的结果创立独立的文件夹、避免 VTPAH 和 GPATH 特殊变量的使用、只要使用 VPATH 就要考虑是否使用 GPATH
¶5、路径搜索的综合示例
¶1) 需求分析
- 工程项目中不希望源码文件夹在编译过程中被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译的结果
- 编译过程能够自动搜索需要的文件
- makefile 易于扩展,能够复用于相同类型的项目
- 支持调试版本的编译选项
¶2) 工具原料
获取 $(DIR) 文件夹中满足 _pattern 的文件
1 | $(wildcard $(DIR)/_pattern) |
去除 _names 中每一个文件名的路径前缀
1 | $(notdir _names) |
将 _text 中符合 _pattern 的部分替换为 replacement
1 | $(patsubst _pattern, replacement, _text) |
¶3) 关键技巧
自动获取源文件列表(函数调用)
1 | SRCS := $(wildcard src/*.c) |
根据源文件列表生成目标文件列表(变量值的替换)
1 | OBJS := $(SRCS:.c=.o) |
替换每一个目标的路径前缀(函数调用)
1 | OBJS := $(patsubst src/%,build/%,$(OBJS)) |
¶4) 编程实现
1 | .PHONY : all clean |
十四、打造专业的编译环境
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
.PHONY : all
DIR_SRC := src
DIR_INC := inc
DIR_BUILD := /home/book/code/build
DIR_COMMON_INC := /home/book/code/common/inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
AR := ar
ARFLAGS := crs
CC := gcc
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1
2
3
4
5
6
7
8
9
10
MODULES := common \
main \
module
test :
@for dir in $(MODULES);\
do\
echo $$dir;\
done
@echo "Compile Success ..."
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1
gcc -o app.out x.a y.a z.a
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1
gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)"
编程实现 Link 阶段
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
.PHONY : all compile link
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
LFLAGS :=
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a,$(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile link
link : $(MODULE_LIB)
@echo "Bengin to link ..."
$(CC) -o $(APP) -Xlinker "-(" $^ -Xlinker "-)" $(LFLAGS)
@echo "Success Target ==> $@ ..."
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "Compile Begin ..."
@set -e;\
for dir in $(MODULES);\
do\
cd $$dir && make all DEBUG:=$(DEBUG) && cd ..;\
done
@echo "Compile Success ..."
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
clean :
$(RM) $(DIR_BUILD)
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
.PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \ # 传递路径给子 makefile
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) && \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include $(MOD_CFG)
# Custmization Begin
#
# DIR_SRC := src
# DIR_INC := inc
#
# TYPE_INC := .h
# TYPE_SRC := .c
# TYPE_OBJ := .o
# TYPE_DEP := .dep
#
# Custmization End
include $(CMD_CFG)
include $(MOD_RULE)
工程目录下的 makefile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
PHONY : all compile link rebuild
MODULES := common \
main \
module
CC := gcc
MKDIR := mkdir
RM := rm -rf
DIR_PROJECT := $(realpath .)
DIR_BUILD := build
DIR_COMMON_INC := common/inc
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
APP := app.out
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
可能改变的变量 mod-cfg.mk
1
2
3
4
5
6
7
DIR_SRC := src
DIR_INC := inc
TYPE_INC := .h
TYPE_SRC := .c
TYPE_OBJ := .o
TYPE_DEP := .dep
定义相对稳定的变量和规则 mod-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
.PHONY : all
MODULE := $(realpath .) # 获取当前路径下的绝对路径
MODULE := $(notdir $(MODULE))
DIR_OUTPUT := $(addprefix $(DIR_BUILD)/,$(MODULE))
OUTPUT := $(MODULE).a
OUTPUT := $(addprefix $(DIR_BUILD)/,$(OUTPUT))
SRCS := $(wildcard $(DIR_SRC)/*$(TYPE_SRC))
OBJS := $(SRCS:$(TYPE_SRC)=$(TYPE_OBJ))
OBJS := $(patsubst $(DIR_SRC)/%,$(DIR_OUTPUT)/%,$(OBJS))
DEPS := $(SRCS:$(TYPE_SRC)=$(TYPE_DEP))
DEPS := $(patsubst $(DIR_SRC)/%, $(DIR_OUTPUT)/%,$(DEPS))
vpath %$(TYPE_INC) $(DIR_INC)
vpath %$(TYPE_INC) $(DIR_COMMON_INC)
vpath %$(TYPE_SRC) $(DIR_SRC)
-include $(DEPS)
all : $(OUTPUT)
@echo "Success! Target ==> $(OUTPUT)"
$(OUTPUT) : $(OBJS)
$(AR) $(ARFLAGS) $@ $^
$(DIR_OUTPUT)/%$(TYPE_OBJ) : %$(TYPE_SRC)
$(CC) $(CFLAGS) -o $@ -c $(filter %$(TYPE_SRC),$^)
$(DIR_OUTPUT)/%$(TYPE_DEP) : %$(TYPE_SRC)
@echo "Creating $@ ..."
@set -e; \
$(CC) $(CFLAGS) -MM -E $(filter %$(TYPE_SRC),$^) | sed 's,\(.*\)\.o[ :]*,$(DIR_OUTPUT)/\1.o $@: ,g' > $@
定义相对稳定的命令 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
AR := ar
ARFLAGS := crs
CC := gcc
LFLAGS :=
CFLAGS := -I$(DIR_INC) -I$(DIR_COMMON_INC)
ifeq ($(DEBUG),true)
CFLAGS += -g
endif
MKDIR := mkdir
RM := rm -rf
定义项目变量以及编译路径变量等 pro-cfg.mk
1
2
3
4
5
6
7
8
9
10
11
12
MODULES := common \
main \
module
MOD_CFG := mod-cfg.mk
MOD_RULE := mod-rule.mk
CMD_CFG := cmd-cfg.mk
DIR_BUILD := build
DIR_COMMON_INC := common/inc
APP := app.out
定义其他变量和规则 pro-rule.mk
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
PHONY : all compile link rebuild
DIR_PROJECT := $(realpath .)
DIR_BUILD_SUB := $(addprefix $(DIR_BUILD)/,$(MODULES))
MODULE_LIB := $(addsuffix .a, $(MODULES)) # 增加后缀 .a
MODULE_LIB := $(addprefix $(DIR_BUILD)/,$(MODULE_LIB))
APP := $(addprefix $(DIR_BUILD)/,$(APP))
all : compile $(APP)
@echo "Target $(APP) success!"
compile : $(DIR_BUILD) $(DIR_BUILD_SUB)
@echo "compile begin ..."
@set -e;\
for dir in $(MODULES); \
do \
cd $$dir && \
$(MAKE) all \
DEBUG:=$(DEBUG) \
DIR_BUILD:=$(addprefix $(DIR_PROJECT)/,$(DIR_BUILD)) \
DIR_COMMON_INC:=$(addprefix $(DIR_PROJECT)/,$(DIR_COMMON_INC)) \
MOD_CFG:=$(addprefix $(DIR_PROJECT)/,$(MOD_CFG)) \
MOD_RULE:=$(addprefix $(DIR_PROJECT)/,$(MOD_RULE))\
CMD_CFG:=$(addprefix $(DIR_PROJECT)/,$(CMD_CFG))&& \
cd .. ;\
done
@echo "compile success!"
$(DIR_BUILD) $(DIR_BUILD_SUB) :
$(MKDIR) $@
link $(APP) : $(MODULE_LIB)
@echo "begin to link !"
$(CC) -o $(APP) -Xlinker "-(" $(MODULE_LIB) -Xlinker "-)"
@echo "link success!"
clean :
$(RM) $(DIR_BUILD)
rebuild : clean all
最后工程 makefile 的内容就很简单了
1
2
3
include pro-cfg.mk # 定义命令相关变量
include cmd-cfg.mk # 定义项目变量以及编译路径变量等
include pro-rule.mk # 定义其他变量和规则
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。
¶1、大型项目目录结构(无第三库)
¶2、需要打造的编译环境
- 源码文件夹在编译时不希望被改动(只读文件夹)
- 在编译时自动创建文件夹(build)用于存放编译结果
- 编译过程中自动生成依赖关系,自动搜索需要的文件
- 每个模块可以拥有自己独立的编译方式
- 支持调试版本的编译选项
¶3、项目架构设计分析
项目根据功能被划分为多个不同模块, 每个模块的代码用一个文件夹进行管理,文件夹由 inc, src, makfile 构成。每个模块的对外函数声明统一放置于 common/inc 中,如:common.h xxfunc.h
¶1)实现阶段分析
由项目的目录结构将整个 makefile 的实现分为两个阶段编译(Compile)和链接(Link)
- Compile 阶段将每个文件中的代码编译成静态库文件
因此 Compile 阶段,我们需要完成可用于各个模块可编译的 makefile 文件,每个模块的编译结果为静态库文件(.a文件)
- link 阶段将每个模块的静态库文件链接生成最终可执行程序
因此 link 阶段,我们需要完成整编译个工程的 makefile 文件,调用模块的 makefile 编译生成静态库文件链接所有的静态库文件,最终得到可执行程序
¶a. Compile 阶段实现
关键的实现要点有三个,自动生成依赖关系(gcc -MM),自动搜索需要的文件(vpath),将目标打包为静态库文件(ar crs) 。模块中的 makefile 构成如下。
编程实现
1 | .PHONY : all |
¶b. Link 阶段实现
Link 阶段需要考虑的事情主要有四个,如何自动创建 build 文件夹以及子文件夹?如何进入每个模块进行编译?编译成功后如何链接所有的模块静态库?当前模块中有哪些模块?对于前三个问题比较简单分别是 使用 mkdir 创建 build 文件夹及子文件夹。使用 cd 命令进入模块编译。编译成功后使用 gcc 链接所有模块的静态库文件。需要解决的注意问题是第四个问题。
在实际的工程里面,项目中各个模块在设计阶段就已经基本确定,因此在在之后的开发过程中不会频繁的随意增加或者减少。因此可以直接定义变量模块名列表(模块变量名),利用 shell 的 for 循环遍历模块变量名,在 for 循环中进入模块文件夹进行编译,循环结束后链接所有的模块静态库文件
在 makefile 中插入 shell 的 for 循环的方式如下,需要注意的是 makefile 中嵌入 shell 代码时,如果需要使用 shell 变量的值,必须在变量前面加上 $$ 。
1 | MODULES := common \ |
链接时需要注意,gcc 在进行静态库的链接时必须遵循严格的依赖关系
1 | gcc -o app.out x.a y.a z.a |
其中的依赖关系必须为:x.a --> y.a --> z.a ,默认情况下遵循自左向右的依赖关系。如果不清楚库间的依赖关系,可以使用 -Xlinker 自动确定依赖关系
1 | gcc -o app.out -Xlinker "-(" z.a y.a x.a -Xlinker "-)" |
编程实现 Link 阶段
1 | .PHONY : all compile link |
¶2) 编译优化
上述 makefile 存在两个潜在问题,首先是所有模块 makefile 中使用编译路径均为写死的绝对路径,一旦项目文件夹移动,编译必将失败
针对这个问题在工程 makefile 中获取项目的源码目录,根据项目源码路径:拼接得到编译文件夹路径 DIR_BUILD,拼接得到全局头文件路径 DIR_COMMON_INC。通过定义命令行变量将路径传递给模块 makefile。优化后的代码如下:
1 | .PHONY : all compile link rebuild |
第二个问题就是,所有 makefile 模块完全相同,当模块 makefile 需要改动时,将涉多处相同改动。针对这个问题,可以将模块 makefile 差分为两个模板文件 mod-cfg.mk 定义为可能改变的变量,mod-rule.mk 定义相对稳定的变量和规则。默认情况下,模块 makefile 复用模板文件实现功能 (include)。这个解决方案需要,通过命令行变量进行模板文件位置的传递。优化后的 makefile 如下:
对于各个模块下的 makefile
1 | include $(MOD_CFG) |
工程目录下的 makefile
1 | PHONY : all compile link rebuild |
可能改变的变量 mod-cfg.mk
1 | DIR_SRC := src |
定义相对稳定的变量和规则 mod-rule.mk
1 | .PHONY : all |
定义相对稳定的命令 cmd-cfg.mk
1 | AR := ar |
经过上述的拆分,已经解决了各个模块下的 makfile 复用的问题。但是对于工程 makefile 模块可以进一步进行优化。 可以拆分命令变量、项目变量、以及其他变量和规则到不同文件。
- cmd-cfg.mk :定义命令相关变量
- pro-cfg.mk :定义项目变量以及编译路径变量等
- pro-rule.mk :定义其他变量和规则
- 最后的工程 makefile 通过包含拆分后的文件构成 (include)
优化后的 makeflie 如下, 首先是定义命令相关变量 cmd-cfg.mk
1 | AR := ar |
定义项目变量以及编译路径变量等 pro-cfg.mk
1 | MODULES := common \ |
定义其他变量和规则 pro-rule.mk
1 | PHONY : all compile link rebuild |
最后工程 makefile 的内容就很简单了
1 | include pro-cfg.mk # 定义命令相关变量 |
到这里一个大型项目的编译环境就基本打造完成了。大型项目的编译环境是由不同的 makefile 构成的。编译环境的设计需要依据项目的整体架构设计。整个项目的编译可以分解为不同阶段。根据不同的阶段有针对性的对 makefile 进行设计。makefile 也需要考虑复用性和维护性等基本程序特性。