Makefile基础(三)

第一章:C语言之Makefile基础(一)

第二章:C语言之Makefile基础(二)

再来看一个简单的例子:

[root@localhost linux_c]# cat Makefile 
foo = $(bar)
bar = Huh?
all:
	@echo $(foo)
[root@localhost linux_c]# make
Huh?

  

我们看到了,bar的值后于foo定义,但执行make的时候仍然打印出Huh?。这说明当make读到foo=$(bar)时,确定foo的值是$(bar),但并不立即展开$(bar),然后读到bar=Huh?,确定bar的值是Huh?,然后在执行规则all:的命令列表时才需要展开$(foo),得到$(bar),再展开$(bar),得到Huh?。因此,虽然bar的定义写在foo之后, $(foo)展开还是能够取到$(bar)的值

这样的特性有好处也有坏处,好处是我们可以把变量的定义推迟到后面定义,如:

main.o: main.c
	$(CC) $(CFLAGS) $(CPPFLAGS) -c $<
CC = gcc
CFLAGS = -O -g
CPPFLAGS = -Iinclude

  

编译命令展开成gcc -O -g -Iinclude -c main.c。通常把CFLAGS定义成一些编译选项,例如-O -g等,而把CPPFLAGS定义成一些预处理选项,如-D -I等,用等到定义变量的延迟展开特性也有坏处,如:

A = $(B)
B = $(A)

  

make有能力检查出这样的错误避免陷入死循环,但有时候我们希望make遇到变量立即展开时,可以用:=运算符,如:

[root@localhost linux_c]# cat Makefile 
x := foo
y := $(x) bar
all:
	@echo "-$(y)-"
[root@localhost linux_c]# make
-foo bar-

  

我们尝试一下把x和y调换一下顺序:

[root@localhost linux_c]# cat Makefile 
y := $(x) bar
x := foo
all:
	@echo "-$(y)-"
[root@localhost linux_c]# make
- bar-

  

当make读到y变量的时候,x尚未定义,展开即为空值,所以y的变量取值是 bar,注意bar前面有一个空格。一个变量的定义从=后面的第一个非空白字符开始(从$(x)的$开始),包括后面的所有字符,直到注释或换行之前结束。如果要定义一个变量的值是一个空格,可以这样:

nullstring :=
space := $(nullstring) # end of the line

  

nullstring的值为空,space的值是一个空格。

还有一个比较有用的运算符是?=,例如foo ?= $(bar)的意思是:如果foo没有定义过,那么?=相当于=,定义foo的值是$(bar),但不立即展开;如果先前已经定义了foo,则什么也不做,不会给foo重新赋值

+=运算符可以给变量追加值,例如:

[root@localhost linux_c]# cat Makefile 
objects = main.o
objects += $(foo)
foo = foo.o bar.o
all:
	@echo "$(objects)"
[root@localhost linux_c]# make
main.o foo.o bar.o

  

现在,让我们看几个常用的特殊变量:

  • $@,表示规则中的目标
  • $<,表示规则中的第一个条件
  • $?,表示规则中所有比目标新的条件,组成一个列表,以空格分隔
  • $^,表示规则中的所有条件,组成一个列表,以空格分隔

例如下面的这条规则:

main: main.o stack.o maze.o
	gcc main.o stack.o maze.o -o main

  

可以改写成:

main: main.o stack.o maze.o
	gcc $^ -o $@

  

这样即使以后往条件里追加了新的文件,编译命令也不用修改

现在,我们的Makefile写成这样:

all: main
main: main.o stack.o maze.o
	gcc $^ -o $@
main.o: main.h stack.h maze.h
stack.o: stack.h main.h
maze.o: maze.h main.h
clean:
	-rm main *.o
.PHONY: clean

  

按照惯例,用all做缺省目标,但还有一点比较麻烦的是,在写main.o、stack.o和maze.o这三个目标时要检查源码,找出他们依赖于哪些头文件,这很容易出错。一是因为有的头文件包含在另一个头文件中,写规则的时候很容易遗漏,二是如果以后修改源代码改变了依赖关系,很可能忘记修改Makefile的规则。为了解决这个问题,可以用gcc -M选项自动生成目标文件和源文件额的依赖关系:

[root@localhost linux_c]# gcc -M main.c 
main.o: main.c /usr/include/stdc-predef.h /usr/include/stdio.h \
 /usr/include/features.h /usr/include/sys/cdefs.h \
 /usr/include/bits/wordsize.h /usr/include/gnu/stubs.h \
 /usr/include/gnu/stubs-64.h \
 /usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stddef.h \
 /usr/include/bits/types.h /usr/include/bits/typesizes.h \
 /usr/include/libio.h /usr/include/_G_config.h /usr/include/wchar.h \
 /usr/lib/gcc/x86_64-redhat-linux/4.8.5/include/stdarg.h \
 /usr/include/bits/stdio_lim.h /usr/include/bits/sys_errlist.h main.h \
 stack.h maze.h

  

-M选项把stdio.h以及它包含的系统头文件也找出来,如果我们不需要输出系统头文件,的依赖关系,可以用-MM选项:

[root@localhost linux_c]# gcc -MM *.c 
main.o: main.c main.h stack.h maze.h
maze.o: maze.c maze.h main.h
stack.o: stack.c stack.h main.h

  

接下来的问题是,怎么把这些规则包含到Makefile文件中,GNU make的官方手册建议这样写:

all: main
main: main.o stack.o maze.o
	gcc $^ -o $@
clean:
	-rm main *.o
.PHONY: clean
sources = main.c stack.c maze.c
include $(sources:.c=.d)
%.d: %.c
	set -e; rm -f $@; \
	$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

    

sources变量包含我们要编译的所有.c文件,$(source:.c=.d)是一个变量替换语法,把source变量中每一项的.c替换成.d,所以include这一句相当于:

include main.d stack.d maze.d

  

类似于C语言中的#include指示,这里的include表示包含3个文件:main.d、stack.d和maze.d,这三个文件也应该符合Makefile的语法,如果现在你的工作目录是干净的,只有.c文件、.h文件和Makefile,运行make的结果是:

[root@localhost linux_c]# make
Makefile:8: main.d: No such file or directory
Makefile:8: stack.d: No such file or directory
Makefile:8: maze.d: No such file or directory
set -e; rm -f maze.d; \
cc -MM  maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$
set -e; rm -f stack.d; \
cc -MM  stack.c > stack.d.$$; \
sed 's,\(stack\)\.o[ :]*,\1.o stack.d : ,g' < stack.d.$$ > stack.d; \
rm -f stack.d.$$
set -e; rm -f main.d; \
cc -MM  main.c > main.d.$$; \
sed 's,\(main\)\.o[ :]*,\1.o main.d : ,g' < main.d.$$ > main.d; \
rm -f main.d.$$
cc    -c -o main.o main.c
cc    -c -o stack.o stack.c
cc    -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

  

一开始找不到.d文件,所以make会报警告,但是make会把include的文件名也当做目标来尝试更新,而这些目标适用规则%.d: %.c,所以执行它的命令列表,比如生成maze.d的命令:

set -e; rm -f maze.d; \
cc -MM  maze.c > maze.d.$$; \
sed 's,\(maze\)\.o[ :]*,\1.o maze.d : ,g' < maze.d.$$ > maze.d; \
rm -f maze.d.$$

  

虽然在Makefile中这个命令写了四行,但其实是一条命令,make只创建了一个shell进程执行这条命令,这条命令分为5个子命令,用;号隔开,执行步骤为:

  1. set -e命令设置当前shell进程为这样的状态:如果它执行的任何一条命令退出状态非0则立刻终止,不再执行后续命令
  2. 把原来的maze.d删掉
  3. 重新生成maze.c的依赖关系,保存成文件maze.d.18006(假设当前shell进程的id是18006),在Makefile中$有特殊含义,如果要表示它的字面意思则需要两个$,所以Makefile中的四个$传给shell会变量两个$,两个$在shell中表示当前的进程id,一般用于给临时文件起名,以保证文件名唯一
  4. sed命令的主要作用就是查找替换,maze.d.18006的内容为:maze.o: maze.c maze.h main.h,经过sed处理后存为maze.d,其内容为:main.o main.d : main.c main.h stack.h maze.h
  5. 最后把临时文件maze.d.123删除

不管Makefile本身还是它包含的文件,只要有一个文件在make过程中被更新,make就会重新读取整个Makefile以及被它包含的所有文件,现在main.d、stack.d和maze.d都生成了,就可以正常包含进来了,相当于在Makefile中添加了三条规则:

main.o main.d: main.c main.h stack.h maze.h
maze.o maze.d: maze.c maze.h main.h
stack.o stack.d: stack.c stack.h main.h

  

如果现在修改了main.c文件,根据规则:main.o main.d: main.c main.h stack.h maze.h要重新生成新的main.o和main.d。生成main.o的规则有两条:

main.o: main.c main.h stack.h maze.h
%.o: %.c
# commands to execute (built-in):
	$(COMPILE.c) $(OUTPUT_OPTION) $<

  

第一条是把规则main.o main.d: main.c main.h stack.h maze.h拆开写得到的,第二条是隐含规则,因此执行cc命令重新编译main.o。生成main.d的规则也有两条: 

main.d: main.c main.h stack.h maze.h
%.d: %.c
	set -e; rm -f $@; \
	$(CC) -MM $(CPPFLAGS) $< > $@.$$$$; \
	sed 's,\($*\)\.o[ :]*,\1.o $@ : ,g' < $@.$$$$ > $@; \
	rm -f $@.$$$$

  

常用make命令行选项:

-n选项只打印要执行的命令,而不会真的执行命令,这个选项有助于我们检查Makefile写得是否正确,由于Makefile不是顺序执行的,用这个选项可以查看命令执行的顺序。

-C选项可以切换另一个目录执行那个目录下的Makefile,比如:

[root@localhost linux_c]# cat ./test/Makefile 
objects = main.o
objects += $(foo)
foo = foo.o bar.o
all:
	@echo "$(objects)"
[root@localhost linux_c]# make -C ./test/
make: Entering directory `/home/lf/linux_c/test'
main.o foo.o bar.o
make: Leaving directory `/home/lf/linux_c/test'

  

一些规模较大的项目会把不同的模块或子系统的源代码放在不同的子目录中,然后在每个子目录下都写一个Makefile文件,然后再用一个总的Makefile文件中用make -C命令执行每个子目录下的Makefile

在make命令行也可以用=或:=定义变量,如果编译的时候我想加入-g,又不想每次编译的时候都加上-g,可以在命令行中定义CFLAGS变量而不必修改Makefile:

[root@localhost linux_c]# cat Makefile 
main: main.o stack.o maze.o
	gcc main.o stack.o maze.o -o main
main.o stack.o: main.h
main.o maze.o: maze.h
main.o stack.o: stack.h
clean:
	-rm main *.o
.PHONY: clean
[root@localhost linux_c]# make CFLAGS=-g
cc -g   -c -o main.o main.c
cc -g   -c -o stack.o stack.c
cc -g   -c -o maze.o maze.c
gcc main.o stack.o maze.o -o main

  

我们再来看一段代码:

[root@localhost linux_c]# cat Makefile 
foo = 1
all:
	@echo $(foo)
[root@localhost linux_c]# make
1
[root@localhost linux_c]# export foo=2
[root@localhost linux_c]# make -e
2

  

我们首先在Makefile文件中定义foo为1,执行make命令,打印出1,这是没有任何问题的,然后我们在环境变量中定义了foo为2,再次执行的时候加上-e这个选项,文件中定义的foo = 1被环境变量的foo=2覆盖

如果我们把foo定义在make后面,会覆盖文件中的foo变量。如果把foo=3写在make前面,则foo=3是shell进程传给make进程的环境变量,而不是命令行选项,要区分第三行和第五行的写法

[root@localhost linux_c]# make foo=3
3
[root@localhost linux_c]# foo=3 make
1
[root@localhost linux_c]# foo=3 make -e
3

  

刚才讲过在一些规模较大的项目中,每个目录底下会有一个Makefile文件,一般是由上层目录的Makefile里用make -C命令执行下层目录的Makefile。我们可以在上层目录的Makefile里用export声明一些变量,这些变量会自动传给make -C命令做环境变量,注意,这里的export不是shell命令,而是Makefile的声明:

[root@localhost linux_c]# cat Makefile 
foo = str1
bar = str2
export foo
all:
	$(MAKE) -C subdir
[root@localhost linux_c]# cat ./subdir/Makefile 
all:
	@echo $(foo) $(bar)
[root@localhost linux_c]# make
make -C subdir
make[1]: Entering directory `/home/lf/linux_c/subdir'
str1
make[1]: Leaving directory `/home/lf/linux_c/subdir'

  

  

posted @ 2018-07-11 05:57  北洛  阅读(237)  评论(0编辑  收藏  举报