我们平时说的代码编译其实是要分为两个部分:编译和链接。编译就是将我们的代码翻译称为二进制文件,链接就是将各个文件所翻译成的二进制文件进行合并和重定位,最终生成可执行文件。由于没有学过编译原理,这里暂时就先了解这么多。
假设我们有一个文件hello.c
,接下来使用该文件对编译进行一些了解。
// example1
#include
void main()
{
printf("HELLO WORLD!\n");
}
编译
三个步骤:
a. 预编译:预编译会将我们的头文件展开、注释删除、#define替换,生成一个(.i)文件,所使用的命令是:
$ gcc -o hello.i -E hello.c
我们cat hello.i
可以看到stdio.h
中的内容已经被展开了,stdio.h
中为我们提供的函数声明都一目了然了,这里就不贴出来了。
b. 编译:编译阶段会把C语言代码翻译成为汇编代码(翻译过程分为 语法分析、词法分析、语义分析、符号汇总四步 (仅做了解)),生成(.s)文件,所使用的命令是:
$ gcc -o hello.s -S hello.i
打开hello.s
可以看到一堆看不懂的汇编代码。
c. 汇编:汇编阶段会做两件事,把汇编代码转换为二进制指令,形成符号表,生成(.o)文件,也就是我们常说的目标文件,使用的命令是,注意命令中是大写的C:
$ gcc -o hello.o -C hello.s
hello.o
文件我们打开会看到一堆乱码,可能有少许可以看懂的明文。
链接
阶段做了两件事情:合并段表、符号表的合并和重定位(暂时仅了解),把目标文件(.o)链接成为可执行程序。
我们平时使用gcc命令并不会单独去执行-E -S -C
,用的比较多的是:
$ gcc -c hello.c # 编译输出目标文件,如果没有指定文件名则生成同名目标文件
$ gcc hello.c # 编译 链接 生成可执行程序
每次编译都要去敲gcc命令还是挺麻烦的,尤其是源文件逐渐变多,情况会变得更糟糕,这时候就需要Makefile
给我们帮忙了。
一个简单的makefile需要目标(编译需要生成的内容),必备条件(依赖),以及规则(shell命令)三个部分:
target : prerequisites
cmd
我们要注意的是cmd前面的是一个tab
,并不是4个空格,如果输入的是空格,那我们执行make
命令时就会报出如下error:
may@ubuntu:~/work/makefile/exec1$ make
Makefile:2: *** missing separator. Stop.
接下来我们就为上面的hello.c
来写一个makefile
:
# makefile1
hello: hello.c
gcc -o hello hello.c
接下来我们执行make
命令,看到gcc
命令执行并打印出来,这就成功完成编译了。
may@ubuntu:~/work/makefile/exec1$ make
gcc -o hello hello.c
may@ubuntu:~/work/makefile/exec1$ ./hello
HELLO WORLD!
我们这里是直接生成的可执行文件,并没有生成.o
目标文件,但是看别人makefile
的写法常常如下:
# makefile2
hello: hello.o
gcc -o hello hello.o
hello.o: hello.c
gcc -c hello.c
执行make
命令后可以看到文件夹下除了生成出可执行文件外,还生成了目标文件。为什么我们不一步到位,还要多生成出一个目标文件呢?我的理解是编译多个文件时,可能会有多个其他文件需要依赖该文件,直接生成.o
文件可以减少该文件的编译次数,另外也用作增量编译的。
我们在上面makefile
的基础上做一些改动,将hello.o
和hello
换个位置:
# makefile3
hello.o: hello.c
gcc -c hello.c
hello: hello.o
gcc -o hello hello.o
这时候我们再去make
,发现只生成了目标文件,可执行文件并没有生成,这是为什么呢?这就要了解下make
的工作方式了:
Makefile
文件,当然文件名也可以是makefile
,文件名用哪一个依个人所好;makefile
后,会将第一个目标作为终极目标,生成终极目标后,make
就不会再工作了,这也就是为什么makefile2
中先写了hello.o
之后不会再生成可执行文件的原因;.o
文件不存在,那么make
会为我们查找生成.o
文件所需要的规则;.o
文件存在,那么make
会我们比较生成.o
文件所依赖的文件的修改时间,如果依赖文件的修改时间比.o
文件晚,那么就会重新生成该.o
文件。我们再为example1
添加一些代码:
// example2
// foo.h
#ifndef __FOO_H__
#define __FOO_H__
void foo();
#endif
// foo.c
#include
void foo() {
printf("HELLO FOO!\n");
}
// hello.c
#include
#include "foo.h"
void main()
{
printf("HELLO WORLD!\n");
foo();
}
makefile
也随之更新:
# makefile4
hello: hello.o \
foo.o
gcc -o hello hello.o foo.o
hello.o: hello.c
gcc -c hello.c
foo.o: foo.c
gcc -c foo.c
可以看到makefile4
的hello
最后一个\
,它代表换行。接下来我们执行make
就可以看到如下gcc命令打印并生成新的可执行文件:
gcc -c hello.c
gcc -c foo.c
gcc -o hello hello.o foo.o
我们随意修改foo.c
中的内容,再执行make
,有如下内容打印:
gcc -c foo.c
gcc -o hello hello.o foo.o
可以看到,hello.c
并没有重新编译,这也就印证了make
工作方式第四点。
有时候我们并不会把终极目标的规则写在最前面,这时候我们可以在makefile
的最前面加上一个标签all
,执行make
命令默认编译出all
所依赖的内容。
# makefile5
all: hello
hello.o: hello.c
gcc -c hello.c -o hello.o
foo.o: foo.c
gcc -c foo.c -o foo.o
hello: hello.o \
foo.o
gcc -o hello hello.o foo.o
.PHONY: all
由于执行make
时实际并不会生成all
这个文件,所以我们称这个标签为伪目标,并用.PHONY
标记该标签。这里有一点要补充,我们平时执行make
命令都是编译默认目标,也就是前面提到makefile
中的第一个目标,我们还可以在make
命令后面指定所要生成的目标,如:
make foo.o
就可以单独编译出foo.o了;同样的如果make
后面跟上标签(伪目标),就会执行伪目标后面的命令了。我们这里可以用make all
来编译出hello
,也可以直接执行make
,这是因为all
本身就是我们的默认目标。
每次执行make都会生成一堆中间文件,有时候想删除他们就会很麻烦,这时候就可以用伪目标帮我们完成这个任务。修改makefile5
,在最后加上一个新的伪目标clean
:
# makefile5.1
.PHONY: clean all
clean:
-rm *.o hello
有了这个伪目标,我们直接执行make clean
就可以清除编译的内容了:
may@ubuntu:~/work/makefile/exec1$ make clean
rm *.o hello
我们可以看到rm
命令前面有一个-
减号,它的作用是尝试执行该命令,如果执行失败不会影响下一个命令的执行。我们继续修改makefile5.1
,在clean后面再加一句echo
,并且移除-
:
# makefile5.2
.PHONY: clean all
clean:
rm *.o hello
@echo "Hello !"
假如我们还没有执行编译命令,当前目录下就没有生成的目标文件,这时候执行rm
命令就会出错:
may@ubuntu:~/work/makefile/exec1$ make clean
rm *.o hello
rm: cannot remove '*.o': No such file or directory
rm: cannot remove 'hello': No such file or directory
Makefile:18: recipe for target 'clean' failed
make: *** [clean] Error 1
可以看到执行rm
出错之后,make
没有继续往下运行,"Hello !"
没有被打印出来。
这里又看到一个新的符号@
,之前使用make
我们看到执行的每一条命令都会打印到终端上,在命令前加上@
,make
执行命令时就不会打印该命令了。
这时候我们还原成makefile5.1
,执行make clean
:
may@ubuntu:~/work/makefile/exec1$ make clean
rm *.o hello
rm: cannot remove '*.o': No such file or directory
rm: cannot remove 'hello': No such file or directory
Makefile:18: recipe for target 'clean' failed
make: [clean] Error 1 (ignored)
Hello !
虽然有出错,但是"Hello !"
还是被打印出来了。
我们可以通过变量去替代依赖或者编译指令中的一些内容,我们将makefile5
改造成makefile6
:
# makefile6
objs = hello.o foo.o
all: hello
hello.o: hello.c
gcc -c hello.c -o hello.o
foo.o: foo.c
gcc -c foo.c -o foo.o
hello: $(objs)
gcc -o hello $(objs)
.PHONY: clean all
clean:
-rm *.o hello
@echo "Hello !"
变量可以放在makefile文件的最上面,并不影响我们的终极目标。有了变量,后期如果我们需要增加编译的文件,可以直接在objs后面加上相应的目标文件,省去了在多个地方添加该依赖的工作。
如果我们中途想要给变量objs
增加一些内容,这时候我们可以使用+=
,当然我们一开始给objs
赋值时也可以使用+=
。
# makefile6.1
a =
a = HELLO
a += WORLD
all: test
test:
@echo ${a}
对于makefile6.1
,我们执行make
命令后就会打印出HELLO WORLD
了。可以看到我们在创建变量时是可以不为它赋值的。
may@ubuntu:~/work/makefile/exec1$ make
HELLO WORLD
我们使用变量时有一种情况是引用其他变量的值$()
,makefile
中的前面的变量可以引用后面的变量(这一点倒是很奇特):
# makefile6.2
a =
a = $(b)
all: test
test:
@echo ${a}
b = HELLO
b += WORLD
对于makefile6.2
,执行make
命令后同样可以打印出HELLO WORLD
,但是a
赋值时b
还没有声明,而且获取到的是b
的最终值。
以下是一种很糟糕的用法,a b
相互嵌套引用,展开时就会出现套娃的情况,这是我们不希望看到的:
a = $(b)
b = $(a)
makefile
提供了:=
来解决这个问题,但是使用该赋值符号就只能引用前面声明的变量了,以下makefile6.3
将不会有任何输出。
# makefile6.3
a :=
a := $(b)
all: test
test:
@echo ${a}
b = HELLO
b += WORLD
我们平时见到使用的更多的是:=
,前面的变量可以引用后面的变量不太符合逻辑~
makefile
为我们提供了三个自动变量来简化makefile
的书写:
$@
:在规则命令中表示目标$<
:在规则命令中表示第一个依赖条件$^
:在规则命令中表示所有依赖条件使用自动变量对makefile6
进行改造,
# makefile7
objs = hello.o foo.o smile.o
all: hello
hello.o: hello.c
gcc -c $^ -o $@
foo.o: foo.c
gcc -c $^ -o $@
smile.o: smile.c
gcc -c $< -o $@
hello: $(objs)
gcc $^ -o $@
.PHONY: clean all
clean:
-rm *.o hello
@echo "Hello !"
可以看到我们把gcc
命令中的所有依赖都用自动变量$^ $<
代替了,当只有一个依赖时这两者可以互换;命令中的输出都用$@
来替代了。
后期如果要添加依赖我们就可以不用修改命令了。
很多时候我们会把源文件和头文件分门别类,makefile
和这些原文件不在一个文件夹之中,这时候要怎么办呢?makefile
为我们提供了两个文件搜寻功能。
VPATH
:VPATH
为我们指定目录,make
如果在当前文件路径下找不到依赖文件,就会到VPATH
执行的路径下去寻找。如果VPATH
指定了多个文件路径,那么会按照先后顺序依次查找。may@ubuntu:~/work/makefile/exec1$ tree
.
├── hello.c
├── include
│ ├── foo.h
│ └── smile.h
├── Makefile
├── obj
└── src
├── foo.c
└── smile.c
如果我们写出如下makefile
:
# makefile8
VPATH := src
objs = hello.o foo.o smile.o
CFLAGS := -I include
all: hello
hello.o: hello.c
gcc $(CFLAGS) -c hello.c -o $@
foo.o: foo.c
gcc -c foo.c -o $@
smile.o: smile.c
gcc -c smile.c -o $@
hello: $(objs)
gcc $^ -o $@
.PHONY: clean all
clean:
-rm *.o hello
注意,这时候我们并没有用自动变量($^
),gcc执行命令时会按照指定命令去执行,执行make -n
查看将要执行的命令,看到依旧是在当前目录查找依赖文件,VPATH
并没有生效。
may@ubuntu:~/work/makefile/exec1$ make -n
gcc -c foo.c -o foo.o
gcc -c smile.c -o smile.o
gcc hello.o foo.o smile.o -o hello
将makefile8
中编译命令中的依赖都改成自动变量:
# makefile8.1
VPATH := src
objs = hello.o foo.o smile.o
CFLAGS := -I include
all: hello
hello.o: hello.c
gcc $(CFLAGS) -c $^ -o $@
foo.o: foo.c
gcc -c $^ -o $@
smile.o: smile.c
gcc -c $< -o $@
hello: $(objs)
gcc $^ -o $@
.PHONY: clean all
clean:
-rm *.o hello
这时候再执行make -n
,看到自动变量已经把VPATH
中的路径自动加上了!
may@ubuntu:~/work/makefile/exec1$ make -n
gcc -c src/foo.c -o foo.o
gcc -c src/smile.c -o smile.o
gcc hello.o foo.o smile.o -o hello
vpath
:注意这是小写的,用法和上面第一点不太一样。vpath
可以用来搜小搜索的范围,到指定文件夹下查找指定类型的文件。使用方法如下:# makefile8.2
vpath %.c src
objs = hello.o foo.o smile.o
CFLAGS := -I include
all: hello
hello.o: hello.c
gcc $(CFLAGS) -c $^ -o $@
foo.o: foo.c
gcc -c $^ -o $@
smile.o: smile.c
gcc -c $< -o $@
hello: $(objs)
gcc $^ -o $@
.PHONY: clean all
clean:
-rm *.o hello
可以看到vpath
的使用方式是vpath file directory
,其中file我们用的是%.c
来表示。这里的%
表示匹配的意思,比如说编译foo.o
需要foo.c
,但是发现当前路径下没有,这时候makefile
发现有一个匹配模式%.c
,那它就会去尝试套用一下,刚好可以找到src/foo.c
。
所以我认为%
是给了makefile
查找文件的一个选择,当找不到文件但又有%
可以用来匹配时,就会用这个模板来套用试试。
这里vpath %.c src
表示只能到src
目录下匹配到.c
结尾的文件。
makefile
为我们提供了一些函数,包括有字符串处理函数系列、文件名操作函数系列、foreach、if、call、origin、shell、make控制函数,这里挑一些非常常用的来做说明。
$(subst from,to,text)
将字符串test
中的from
字串替换为to
字串,接下来给出一个错误示例:
a:= Hello World
b:=
test:
b:= $(subst World, Makefile, $(a))
@echo $(b)
执行make
命令可以看到error:
may@ubuntu:~/work/makefile/exec1$ make test
b:= Hello Makefile
/bin/sh: 1: b:=: not found
Makefile:15: recipe for target 'test' failed
make: *** [test] Error 127
为什么World被成功替换了,但时还出错了呢?从错误提示中可以发现,b:=
被解析成为命令。我们所写的规则都是一条一条的shell命令,由于b:= Hello Makefile
不是一条shell命令,所以会报错。
我们在规则中可以使用makefile
为我们提供的函数,但是需要保证,这些函数返回的结果可以让表达式成为一条正确的shell命令。
a:= Hello World
b:= $(subst World, Makefile, $(a))
test:
@echo $(a)
@echo $(b)
修改成以上写法就可以正确执行了:
may@ubuntu:~/work/makefile/exec1$ make test
Hello World
Hello Makefile
以上是匹配的一个完整的字符串,那subst
可以替换字符串中的一部分吗?答案是可以的,示例如下:
a:= abcdef
b:= $(subst cd,oo,$(a))
test:
@echo $(b)
# 输出结果
# abooef
可以看到subst
是可以匹配到字符串中的子串并完成替换的。
$(patsubst pattern,replace,text)
同样是将字符串test
中的pattern
字串替换为replace
字串,这和前面的subst
有什么区别呢?相比前一个,patsubst
多了一个pat
前缀,这是partten
的缩写,意为模式(模板)替换,需要和上面提到的%
搭配使用。接下来给出一个使用示例:
a:= a.c b.c c.o
b:= $(patsubst %.c, %.o, $(a))
test:
@echo $(a)
@echo $(b)
# 输出结果
# a.c b.c c.o
# a.o b.o c.o
patsubst
帮助我们用模板匹配了所有.c
结尾的字串,并替换为了.o
结尾字串。当然replace
也可以不用%
来做匹配,被匹配的字串都会被替换为replace
。text
中的字符串以空格分割,如果没有空格则认为是一个字串。
$(findstring find,text)
查找text
中的find
字符,如果可以找到则返回find
字串,否则返回空字串。
a:= a.c b.c c.o
b:= $(findstring a.c,$(a))
c:= $(findstring d.c,$(a))
test:
@echo $(a)
@echo $(b)
@echo $(c)
# 输出结果
# a.c b.c c.o
# a.c
#
以上示例查找的是一个完整的字符串,那它可以查找子串吗?答案是可以的,示例如下:
a:= abcdef
b:= $(findstring bc,$(a))
test:
@echo $(b)
# 输出结果
# bc
$(filter pattern...,text)
从text
中过滤出pattern
,注意这里可以使用模式匹配,也可以同时匹配多个参数。如果可以成功匹配,则返回匹配到的字串,否则返回空。看起来filter
和findstring
功能类似,但是他们还是有区别的:
filter
可以同时查找多个字串;filter
只能过滤完整的字符串,并不能过滤子串;如果是过滤完整的字串,那么功能和findstring
相同。a:= a.c b.c c.o
b:= $(filter %.c,$(a))
c:= $(filter c.o,$(a))
d:= $(filter c.o a.c,$(a))
e:= $(filter c, $(a))
test:
@echo $(a)
@echo $(b)
@echo $(c)
@echo $(d)
@echo $(e)
# 输出结果
# a.c b.c c.o
# a.c b.c
# c.o
# a.c c.o
#
执行make
命令后可以看到e是空的,也就是没有过滤到内容
$(word n,text)
$(wordlist start,end,text)
word
用于获取text
的第n个字符串,从1开始数;注意这里截取的是字符串,不是单词!
wordlist
用于获取从start开始,end结束的单词。
a:= a.c b.c c.o d.o e.o
b:= $(word 3,$(a))
c:= $(wordlist 2,4,$(a))
test:
@echo $(b)
@echo $(c)
执行make
命令后看到以下输出:
may@ubuntu:~/work/makefile/exec1$ make test
c.o
b.c c.o d.o
$(dir path...)
dir
函数用于返回最后一个/
前面的内容,也就是文件目录,使用示例如下:
a:= $(shell pwd)
b:= $(dir $(a))
test:
@echo $(a)
@echo $(b)
# 输出结果
# /home/may/work/makefile/exec2/foo
# /home/may/work/makefile/exec2/
$(notdir path...)
与dir
相反,notdir
返回最后一个/
后面的部分,也就是文件名,使用示例如下:
a:= $(shell pwd)
b:= $(dir $(a))
test:
@echo $(a)
@echo $(b)
# 输出结果
# /home/may/work/makefile/exec2/foo
# foo
$(suffix dirs...)
suffix
可以获取到dir(...
表示可以为多个参数)文件的后缀:
a:= a.c b.c c.o d.o e.o
b:= $(suffix $(a))
test:
@echo $(b)
# 输出结果
# .c .c .o .o .o
$(basename dirs...)
basename
可以获取到文件名的前缀:
a:= a.c b.c c.o d.o e.o
b:= $(basename $(a))
test:
@echo $(b)
# 输出结果
# a b c d e
$(addsuffix suffix,dirs...)
addsuffix
帮助我们添加文件后缀:
a:= a b c d e
b:= $(addsuffix .c,$(a))
test:
@echo $(b)
# 输出结果
# a.c b.c c.c d.c e.c
$(addsuffix prefix,dirs...)
addprefix
帮助我们添加前缀:
a:= a b c d e
b:= $(addprefix p,$(a))
test:
@echo $(b)
# 输出结果
# pa pb pc pd pe
$(join list1...,list2...)
上面的addsuffix addprefix
也算是字符串拼接,但是只能给字符串列表拼接上相同的字符串。如果想要给列表中的字符串拼接不同的内容,那么就需要使用join
函数了。
join
表示将list2中的每个字串对应拼接到list1后面。
举以下例子,给subsystem添加.o后缀,再添加与文件名相同的路径:
subsystem:= foo smile hello
objs:= $(join $(addsuffix /,$(subsystem)),$(addsuffix .o,$(subsystem)))
# 输出结果
# foo/foo.o smile/smile.o hello/hello.o
可以看到添加相同的后缀可以使用addsuffix
,而添加不同的文件路径则需要用到join
来完成。
$(wildcard patt)
在makefile
的规则当中,通配符会自动展开,变量定义中的通配符也会自动展开,但是在函数引用中,通配符并不会起作用,需要使用wildcard
来做展开。举例如下:
a:= $(wildcard src/*.c)
b:= src/*.c
c:= $(patsubst %.c,%.o,src/*.c)
d:= $(patsubst %.c,%.o,$(wildcard src/*.c))
test:
@echo $(a)
@echo $(b)
@echo $(c)
@echo $(d)
# 输出结果
src/foo.c src/smile.c
src/foo.c src/smile.c
src/*.o
src/foo.o src/smile.o
变量定义b
中的通配符有正常展开,但是c
中的通配符没有正常展开,替换为wildcard
恢复正常。
有很多博客中认为%
是通配符,了解到这里我认为是不对的,通配符*
可以展开获取到变量值,但是%
只能做匹配使用,看做成是一个模板。
$(foreach var,list,express)
foreach
函数用于遍历列表并对内容做相应处理,意为遍历list
中的内容,放到变量var
中,最后执行express
表达式,最后将处理结果依次返回。举例如下:
a:= a b c d e
b:= $(foreach n,$(a),$(addsuffix .c,$(n)))
c:= $(foreach n,$(a),$(n).o)
test:
@echo $(b)
# 输出结果
# a.c b.c c.c d.c e.c
# a.o b.o c.o d.o e.o
$(if condition,then)
$(if condition,then,else)
if
我们应该很熟悉了,如果满足condition
条件,就执行then
中的内容,否则执行else
中的内容。但是,这里的condition
要如何判断呢?
这里if
函数的意义是,如果condition
字串不为空,就计算then
表达式中的内容并返回,否则计算else
中的内容并返回。
a:= abcd
b:= $(if $(a),a,b)
test:
@echo $(b)
# 输出结果
# a
$(shell cmd;cmd2;...)
使用shell
函数可以帮助我们执行shell命令,多条shell命令可以用;
隔开。这里我们就可以使用awk、sed
等命令了。
a:= $(shell pwd;echo HELLO WORLD)
test:
@echo $(a)
# 输出结果
# /home/may/work/makefile/exec1 HELLO WORLD
$(error text...)
error
可以帮助我们打印错误信息,并且终止make
运行:
a:= error
ifeq ($(a),error)
$(error error is a)
endif
test:
@echo $(b)
# 输出结果
# Makefile:14: *** error is a. Stop.
以上例子可以看到无论执行什么都会中止退出。为什么我们执行make test
,没有用到a
变量,make
还是会退出呢?我的猜想是执行make
时会首先扫一遍makefile
,这时候初始化变量并处理一些函数或者表达式。
另外我发现所有的函数并不能直接使用,必须要在赋值语句中使用(除了这里的控制函数),否则会有error。
$(warning text...)
warning
可以帮助我们打印debug信息,并且不会终止make
运行:
a:= HELLO WORLD
test:
@echo $(a)
b := $(shell echo HELLO)
ifneq ($(b),)
$(warning b is $(b))
endif
我们执行make test
命令可以看到如下输出:
may@ubuntu:~/work/makefile/exec1$ make test
Makefile:18: b is HELLO
HELLO WORLD
有两点需要注意:1) warning
不会中止make
的运行;2) 虽然warning
写在test之后,但是执行命令时却先打印出来,所以看起来会先扫整个makefile文件。
ifeq (value1, value2)
express
else
express
endif
使用ifeq ifneq
可以让我们根据条件判断执行不同的编译任务,要注意哦ifeq
和后面的括号之间是有一个空格的,示例如下:
test:
@echo TEST
a:= true
ifeq ($(a),true)
$(warning a is true)
else
$(warning a is false)
endif
我们执行make test
命令可以看到如下输出:
may@ubuntu:~/work/makefile/exec1$ make test
Makefile:19: a is true
TEST
我们在第6节了解了自动变量并且改造了我们的makefile
;第七节了解了文件搜寻,在vpath
部分了解了模式规则%
;第八节学习了很多函数,接下来我们就要用这些函数,搭配模式规则(模板)再去改造makefile
。
目前我们的makefile demo版本停在了makefile8.2
,在这个版本上,我们如果想要增加新的文件的编译,就要为新的文件增加编译规则,例如我要再加入sunny.c
的编译,我们需要修改如下内容:
objs = hello.o foo.o smile.o sunny.o
sunny.o: sunny.c
gcc -c $^ -o $@
随着工程规模的变大,需要修改的内容越来越多,我们有没有办法简化工作呢?那就需要使用模式规则了。
vpath
一节中有说到,make
在当前路径找不到依赖文件时就会到vpath
指定的目录下尝试用模式规则去匹配。
用这个特点我们来改造makefile
:
# makefile9
D_SRC:= ./src
srcs:= $(wildcard $(D_SRC)/*.c)
objs:= $(patsubst %.c,%.o,$(srcs))
CFLAGS := -I include
all: hello
%.o: %.c
gcc $(CFLAGS) -c $^ -o $@
hello: $(objs)
gcc $^ -o $@
.PHONY: clean all
clean:
-rm $(objs) hello
执行make test
命令可以看到如下输出:
may@ubuntu:~/work/makefile/exec1$ make
gcc -I include -c src/foo.c -o src/foo.o
gcc -I include -c src/hello.c -o src/hello.o
gcc -I include -c src/smile.c -o src/smile.o
gcc src/foo.o src/hello.o src/smile.o -o hello
以上改造基于以下两点:
src
下,使用通配符获取到所有的源文件;patsubst
来完成,当然我们也可以使用basename addsuffix
来完成,效果是相同的。从执行过程打印的命令来看,我们的源文件都自带了src路径,输出的.o也在对应的源文件目录下。这一点和vpath
不一样,vpath
找到的文件,编译后的目标文件会在当前makefile
目录下。
我理解的模式规则的工作过程:
hello
需要依赖src/foo.o
,这时候并没有这个文件,就去查找有没有对应的匹配模板;%.o: %.c
,尝试匹配为src/foo.o: src/foo.c
,刚好可以;所有需要的.o
文件我们都可以用以上模式规则来给它编译出来,以后我们要加新的文件进来,就不用再给这个文件加对应的规则了,是不是很方便。
上面的模式规则有没有缺点呢?答案是有的,上面最后我们说了,所有的.o
文件都会套用一个模式规则来编译,这时候我们有一个文件需要用其他的规则来编译,这时候应该怎么办呢?
makefile
为我们提供了静态模式规则来解决这个问题。静态模式我的理解就是让模式规则只适用于指定的文件。静态模式的使用方式如下:
targets:target-pattern:prereq-pattern
command
可以看到我们在原来的目标target-pattern
前面新增了targets
作为限定,意思是只有targets
中的内容才能匹配该模式规则。接下来举个例子,tree如下:
may@ubuntu:~/work/makefile/exec1$ tree
.
├── include
│ ├── foo.h
│ ├── rainny.h
│ └── smile.h
├── Makefile
├── obj
├── rainny.c
└── src
├── foo.c
├── hello.c
└── smile.c
我们在makfile9
的基础上新增一个rainny.c
,如果我们直接执行make
命令,可以看到如下打印:
gcc -I include -c src/foo.c -o src/foo.o
gcc -I include -c src/hello.c -o src/hello.o
gcc -I include -c src/smile.c -o src/smile.o
gcc -I include -c rainny.c -o rainny.o
gcc src/foo.o src/hello.o src/smile.o rainny.o -o hello
可以看到rainny.c
的编译也是套用了带include
的模式规则,但是现在我们不希望它用已经定义的模式规则来编译,接下来修改makefile
:
# makefile9.1
D_SRC:= ./src
srcs:= $(wildcard $(D_SRC)/*.c)
objs:= $(patsubst %.c,%.o,$(srcs))
CFLAGS := -I include
all: hello
$(objs):%.o: %.c
gcc $(CFLAGS) -c $^ -o $@
%.o: %.c
gcc -c $^ -o $@
hello: $(objs) rainny.o
gcc $^ -o $@
.PHONY: clean all
clean:
-rm $(objs) *.o hello
再次执行make
,看到如下打印:
gcc -I include -c src/foo.c -o src/foo.o
gcc -I include -c src/hello.c -o src/hello.o
gcc -I include -c src/smile.c -o src/smile.o
gcc -c rainny.c -o rainny.o
gcc src/foo.o src/hello.o src/smile.o rainny.o -o hello
我们可以看到src
下的文件没有套用%.o: %.c
,而是使用了静态模式规则$(objs):%.o: %.c
;rainny.c
套用了%.o: %.c
,而没有使用静态模式规则$(objs):%.o: %.c
。
从这里我们就可以了解到,静态模式规则是为某些目标编译而设定的特殊匹配方式,如果定义了静态模式规则,就不会使用通用的模式规则,当然如果没有为目标定义静态模式规则,也不能强行使用别人的。
常常在makefile
文件中看到有一些我们没有定义的变量,例如CC、CXX
等,这时候就会疑惑他们都是怎么来的?他们是makefile
提供的一些有预设值的变量,如果我们不对这些值进行设定,使用到这些变量就会使用其默认值。
以下是一些规则命令中常见的默认命令变量:
命令 | 意 义 | 默认命令 |
---|---|---|
AR | 静态库打包命令 | ar |
CC | C语言编译器 | cc |
CXX | C++编译器 | g++ |
CPP | C预处理器 | $(CC) -E |
RM | 删除命令 | rm -f |
LD | 链接器 | ld |
MAKE | make命令 | make |
常见的一些默认命令参数变量:
参数 | 意 义 | 默认参数 |
---|---|---|
CFLAGS | C编译器参数 | |
CXXFLAGS | C++编译器参数 | |
TARGET_ARCH | 目标平台参数 | |
OUTPUT_OPTION | 输出的命令行参数 | -o $@ |
LDFLAGS | 链接器参数 |
其他的makefile默认变量
参数 | 意 义 | 默认参数 |
---|---|---|
CURDIR | 当前makefile执行目录 | 等价于$(shell pwd) |
以上示例中我们只用了一个makefile
文件来控制编译,随着工程规模的变大,我们可能会将makefile
放在不同的目录下,我们可以用include
将其他makefile
包含进来:
include mks...
举个例子,有如下tree:
may@ubuntu:~/work/makefile/exec2$ tree
.
├── foo
│ ├── foo.c
│ └── foo.mk
├── include
│ ├── foo.h
├── Makefile
# Makefile
vpath %c foo
include foo/Makefile
.PHONY: clean all
clean:
-rm *.o
# foo.mk
foo.o: foo.c
$(CC) -c $(CLFAGS) $^ $(OUTPUT_OPTION)
执行make
命令可以看到如下输出,可以成功编译出foo.o
:
may@ubuntu:~/work/makefile/exec2$ make
cc -c foo/foo.c -o foo.o
include
实际是把被包含文件中的内容拷贝过来了,如果没有第一行的vpath
,那么我们编译时就会出错:
make: *** No rule to make target 'foo.c', needed by 'foo.o'. Stop.
提示找不到’foo.c
,这是因为执行make
命令并不是在foo目录下执行的。
第11节中引用其他makefile
本质上是将其他makefile
拷贝过来,在当前目录下执行。随着工程规模的变大,我们可能为每个子模块都写一个makefile
,从而方便管理。编译时我们在根目录执行make
命令,会同时执行所有子模块的makefile
,这被称为嵌套执行make。
举个例子,代码已上传至github,参考 github :
我们认为foo hello smile
是三个单独的工程,并且三个目录下都有一个makefile
可以编出对应的.o文件。
根目录下也有一个makefile
,用它可以编出我们的可执行文件,我们主要来看它:
target:= helloworld
all: $(target)
subsystem:= foo smile hello
objs:= $(join $(addsuffix /,$(subsystem)),$(addsuffix .o,$(subsystem)))
CFLAGS:= -w
$(objs):
@cd $(dir $@);$(MAKE)
$(target): $(objs)
$(CC) $(CFLAGS) $^ -o $@
.PHONY: clean $(subsystem) all
clean:
-rm $(objs) $(target)
foo hello smile
;CFLAGS:= -w
表示执行命令时打印出debug信息;objs
列表中的每个文件都定义了一个规则cd $(dir $@);$(MAKE)
,如果一个命令需要依赖上一个命令的执行,可以将他们放在一行,用; 或者 &&
隔开,这里表示进入到对应的文件夹下,并且执行make
命令;helloworld
需要依赖objs
列表中的目标文件,如果没有,就执行对应的目标标签,生成目标文件;这里的第四点中进行了嵌套执行make,我们尝试执行下make
命令可以看到以下输出:
may@ubuntu:~/work/makefile/exec2$ make
make[1]: Entering directory '/home/may/work/makefile/exec2/foo'
cc -c foo.c -o foo.o
make[1]: Leaving directory '/home/may/work/makefile/exec2/foo'
make[1]: Entering directory '/home/may/work/makefile/exec2/smile'
gcc -c smile.c -o smile.o
make[1]: Leaving directory '/home/may/work/makefile/exec2/smile'
make[1]: Entering directory '/home/may/work/makefile/exec2/hello'
gcc -I ../include -c hello.c -o hello.o
make[1]: Leaving directory '/home/may/work/makefile/exec2/hello'
cc -w foo/foo.o smile/smile.o hello/hello.o -o helloworld
可以成功编译出helloworld
,由于加入了-w
参数,我们进入目录退出目录都有打印出来。
执行make
命令时,系统中的环境变量会被带入到makefile
中:
test:
@echo $(TEST_VAR)
我们在终端中执行如下命令:
may@ubuntu:~/work/makefile/exec2$ export TEST_VAR="hello make"
may@ubuntu:~/work/makefile/exec2$ make
hello make
TEST_VAR
可以在打印出来,说明该环境变量被带入到了makefile
中。
如果出现嵌套makefile
的情况,系统环境变量通用可以带入到嵌套makefile
中。
现在有一个问题,嵌套makefile
可以识别到外层makefile
中的变量吗?答案是可以的,但是需要使用export来导出环境变量,这个环境变量可以被内层的makefile
识别到,这个环境变量在make
命令执行完成后就消失了。
使用export
导出环境变量有两种方式:
# 1
export subsystem
# 2
export
第一种会导出subsystem
这个变量;第二种会导出当前makefile
种所有的变量。这里的例子可以用12节中的demo测试,这里就不再写了。
至此,几天的makefile
学习就暂时告一段落,以后再看到花里胡哨的,不像人写的makefile
文件就不会再慌了,自己也可以尝试写出不像人写的makefile
了~
在实际使用过程中如果碰到什么问题,或者见到什么新的比较常用的用法会再补充过来。