一文读懂Makefile

1 程序的编译和链接

我们平时说的代码编译其实是要分为两个部分:编译和链接。编译就是将我们的代码翻译称为二进制文件,链接就是将各个文件所翻译成的二进制文件进行合并和重定位,最终生成可执行文件。由于没有学过编译原理,这里暂时就先了解这么多。

假设我们有一个文件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给我们帮忙了。


2 简单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文件可以减少该文件的编译次数,另外也用作增量编译的。


3 make的工作方式

我们在上面makefile的基础上做一些改动,将hello.ohello换个位置:

# makefile3
hello.o: hello.c
	gcc -c hello.c

hello: hello.o
	gcc -o hello hello.o

这时候我们再去make,发现只生成了目标文件,可执行文件并没有生成,这是为什么呢?这就要了解下make的工作方式了:

  1. make获取搜寻命令执行目录下的Makefile文件,当然文件名也可以是makefile,文件名用哪一个依个人所好;
  2. 找到makefile后,会将第一个目标作为终极目标,生成终极目标后,make就不会再工作了,这也就是为什么makefile2中先写了hello.o之后不会再生成可执行文件的原因;
  3. 如果所依赖的.o文件不存在,那么make会为我们查找生成.o文件所需要的规则;
  4. 如果.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

可以看到makefile4hello最后一个\,它代表换行。接下来我们执行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工作方式第四点。


4 伪目标

有时候我们并不会把终极目标的规则写在最前面,这时候我们可以在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 !"还是被打印出来了。


5 变量

我们可以通过变量去替代依赖或者编译指令中的一些内容,我们将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

我们平时见到使用的更多的是:=,前面的变量可以引用后面的变量不太符合逻辑~


6 自动变量

makefile为我们提供了三个自动变量来简化makefile的书写:

  1. $@:在规则命令中表示目标
  2. $<:在规则命令中表示第一个依赖条件
  3. $^:在规则命令中表示所有依赖条件

使用自动变量对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命令中的所有依赖都用自动变量$^ $<代替了,当只有一个依赖时这两者可以互换;命令中的输出都用$@来替代了。

后期如果要添加依赖我们就可以不用修改命令了。


7 文件搜寻

很多时候我们会把源文件和头文件分门别类,makefile和这些原文件不在一个文件夹之中,这时候要怎么办呢?makefile为我们提供了两个文件搜寻功能。

  1. VPATHVPATH为我们指定目录,make如果在当前文件路径下找不到依赖文件,就会到VPATH执行的路径下去寻找。如果VPATH指定了多个文件路径,那么会按照先后顺序依次查找。
    举个例子:我们有如下source tree:
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
  1. 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结尾的文件。


8 函数

makefile为我们提供了一些函数,包括有字符串处理函数系列、文件名操作函数系列、foreach、if、call、origin、shell、make控制函数,这里挑一些非常常用的来做说明。

8.1 字符串处理函数系列

8.1.1 字符串替换函数 subst

$(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是可以匹配到字符串中的子串并完成替换的。

8.1.2 模式字符串替换函数 patsubst

$(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也可以不用%来做匹配,被匹配的字串都会被替换为replacetext中的字符串以空格分割,如果没有空格则认为是一个字串。

8.1.2 字符串查找函数 findstring

$(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

8.1.3 过滤函数 filter

$(filter pattern...,text)

text中过滤出pattern,注意这里可以使用模式匹配,也可以同时匹配多个参数。如果可以成功匹配,则返回匹配到的字串,否则返回空。看起来filterfindstring功能类似,但是他们还是有区别的:

  1. filter可以同时查找多个字串;
  2. 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是空的,也就是没有过滤到内容

8.1.4 字符串截取函数 word wordlist

$(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

8.2 文件名操作函数系列

8.2.1 取目录函数 dir

$(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/

8.2.2 取文件函数 notdir

$(notdir path...)

dir相反,notdir返回最后一个/后面的部分,也就是文件名,使用示例如下:

a:= $(shell pwd)
b:= $(dir $(a))
test:
	@echo $(a)
	@echo $(b)
# 输出结果
# /home/may/work/makefile/exec2/foo
# foo

8.2.3 取后缀函数 suffix

$(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

8.2.4 取前缀函数 basename

$(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

8.2.5 加后缀函数 addsuffix

$(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

8.2.6 加前缀函数 addprefix

$(addsuffix prefix,dirs...)

addprefix帮助我们添加前缀:

a:= a b c d e
b:= $(addprefix p,$(a))
test:
	@echo $(b)
# 输出结果
# pa pb pc pd pe

8.2.7 字符串拼接函数 join

$(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来完成。

8.2.8 通配符函数 wildcard

$(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恢复正常。

有很多博客中认为%是通配符,了解到这里我认为是不对的,通配符*可以展开获取到变量值,但是%只能做匹配使用,看做成是一个模板。

8.3 foreach函数

$(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

8.4 if函数

$(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

8.5 shell函数

$(shell cmd;cmd2;...)

使用shell函数可以帮助我们执行shell命令,多条shell命令可以用;隔开。这里我们就可以使用awk、sed等命令了。

a:= $(shell pwd;echo HELLO WORLD)
test:
	@echo $(a)
# 输出结果
# /home/may/work/makefile/exec1 HELLO WORLD

8.6 make控制函数

8.6.1 error函数

$(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

8.6.2 warning函数

$(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文件。

8.6.3 条件编译ifeq ifneq

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

9 模式规则

我们在第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

以上改造基于以下两点:

  1. 我们知道源文件路径在src下,使用通配符获取到所有的源文件;
  2. 对于所有的源文件,我们要编出对应的目标文件,用学到的字串函数patsubst来完成,当然我们也可以使用basename addsuffix来完成,效果是相同的。

从执行过程打印的命令来看,我们的源文件都自带了src路径,输出的.o也在对应的源文件目录下。这一点和vpath不一样,vpath找到的文件,编译后的目标文件会在当前makefile目录下。

我理解的模式规则的工作过程:

  1. hello需要依赖src/foo.o,这时候并没有这个文件,就去查找有没有对应的匹配模板;
  2. 找到有这样一个模板%.o: %.c,尝试匹配为src/foo.o: src/foo.c,刚好可以;
  3. 执行对应的编译规则。

所有需要的.o文件我们都可以用以上模式规则来给它编译出来,以后我们要加新的文件进来,就不用再给这个文件加对应的规则了,是不是很方便。

9.1 静态模式规则

上面的模式规则有没有缺点呢?答案是有的,上面最后我们说了,所有的.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: %.crainny.c套用了%.o: %.c,而没有使用静态模式规则$(objs):%.o: %.c

从这里我们就可以了解到,静态模式规则是为某些目标编译而设定的特殊匹配方式,如果定义了静态模式规则,就不会使用通用的模式规则,当然如果没有为目标定义静态模式规则,也不能强行使用别人的。


10 makefile中的默认变量

常常在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)

11 引用其它Makefile

以上示例中我们只用了一个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目录下执行的。


12 嵌套执行make

第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)
  1. 先定义一个子系统的变量,包含foo hello smile
  2. 运用字符串函数做一些拼接,得到子系统生成的目标文件的文件名(包含目录);
  3. CFLAGS:= -w 表示执行命令时打印出debug信息;
  4. objs列表中的每个文件都定义了一个规则cd $(dir $@);$(MAKE),如果一个命令需要依赖上一个命令的执行,可以将他们放在一行,用; 或者 && 隔开,这里表示进入到对应的文件夹下,并且执行make命令;
  5. 编写规则,终极目标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参数,我们进入目录退出目录都有打印出来。


13 环境变量

执行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测试,这里就不再写了。


14 总结

至此,几天的makefile学习就暂时告一段落,以后再看到花里胡哨的,不像人写的makefile文件就不会再慌了,自己也可以尝试写出不像人写的makefile了~

在实际使用过程中如果碰到什么问题,或者见到什么新的比较常用的用法会再补充过来。

你可能感兴趣的:(编程语言,c语言,c++)