原创 Makedile之四 书写规则

2009-6-26 11:17 2215 9 9 分类: MCU/ 嵌入式

#书写规则

    规则包含两个部分: 一个是依赖关系, 一个是生成目标的方法.
    在Makefile中, 规则的顺序是很重要的! Makefile中只有一个最终目标, 其它的目标都是被
这个目标所连带出来的, 所以一定要让make知道你的最终目标是什么. 一般定义在Makefile中的
目标可能会有很多, 第一条规则中的目标为最终的目标. 如果第一条规则中的目标有很多个, 那
么, 第一个目标会成为最终的目标. make所完成的也就是这个目标.

一. 规则举例

foo.o: foo.c defs.h                # foo模块
    cc -c -g foo.c

foo.o是我们的目标, foo.c和defs.h是目标所依赖的源文件, 而只有一个命令 cc -c -g foo.c
以Tab键开头. 这个规则告诉我们两件事:
    1. 文件的依赖关系, foo.o依赖于foo.c和defs.h的文件, 如果foo.c和defs.h的文件日期要
比foo.o文件日期要新, 或是foo.o不存在, 那么依赖关系发生.
    2. 如果生成(或更新)foo.o文件, 也就是那个cc命令, 说明了如何生成foo.o这个文件.


二. 规则的语法

targets: prerequisites
    command
...

或是这样:
targets: prerequisites; command
    command
...

    targets是文件名, 以空格分开, 可以使用通配符. 一般来说,我们的目标基本上是一个文件
但也有可能是多个文件.
    command是命令行, 如果其不与 "target: rerequisites" 在一行, 必须以Tab键开头! 如果
和prerequisites在一行, 那么可以用分号做为分隔.
    prerequisites是目标所依赖的文件(或依赖目标). 如果其中的某个文件要比目标文件新,那
么, 目标被认为是过时的, 需要重生成的.
    如果命令太长, 你可以使用反斜框\作为换行符. make对一行上有多少个字符没有限制.规则
告诉make两件事: 文件的依赖关系和如何成成目标文件.
    一般来说, make会以UNIX的标准Shell, 也就是/bin/sh来执行命令.

三. 在规则中使用通配符

    如果我们想定义一系列比较类似的文件, 我们很自然地就想起使用通配符. make支持三各通
配符: *, ? 和... ,这和Unix的B-Shell是相同的.
    波浪号 "~" 字符在文件名中也有比较特殊的用途. 如果是 "~/test" ,这就表示当前用户的
$HOME目录下的test目录. 而 "~hchen/test" 则表示用户hchen的宿主目录下的test目录.
    需要我们注意的是, 如果我们的文件名中有通配符, 如: "*" , 那么可以用转义字符\, 如
"\*" 来表示*字符.

例如:
clean:
    rm -f *.o
这是Shell所支持的通配符, 是在命令中的通配符.

print: *.c
    lpr -p $?
    touch print
这个例子说明通配符也可以在我们的规则中, 目标print依赖于所有的.c文件.其中的$?是一个自
动化变量.

objects = *.o
这个例子表示通符同样可以用在变量中.并不是说*.o会展开. objects的值就是*.o. Makefile中
的变量其实就是C中的宏. 如果你要让通配符在变量中展开, 也就是让objects的值是所有.o的文
件名的集合, 可以这样:

objects := $(wildcard *.o)
这种用法由关键字wildcard指出, 关于Makefile的关键字, 将在后面讨论.

四. 文件搜寻

    工程中有大量的源文件时, 我们要把源文件分类存放在不同的目录中. 所以, 当make需要去
找寻文件的依赖关系时, 可以在文件前加上路径, 但最好的方法是把一个路径告诉make, 让make
自动去找.
    Makefile文件中的特殊变量VPATH就是完成这个功能的, 如果没有指明这个变量,make只会在
当前的目录中去找寻依赖文件和目标文件. 如果定义了这个变量, make在当前目录找不到时, 到
所指定的目录中去查找文件.

VPATH = src:../headers
上面的的定义指定两个目录. src和../headers, make会按照这个顺序进行搜索.目录由冒号分隔
    另一个设置文件搜索路径的方法是使用make的vpath关键字,他不是变量, 而是make的关键字
和上面提到的那个VPATH变量很类似, 但是它更为灵活. 它可以指定不同的文件在不同的搜索目
录中. 它的使用方法有三种:
    1. vpath <pattern> <directories>
    为符合模式<pattern>的文件指定搜索目录<directories>.
    
    2. vpath <pattern>
    清除符合模式<pattern>的文件的搜索目录.

    3 vpath
    清除所有已被设置好了的文件搜索目录.

    vapth使用方法中的<pattern>需要包含 "%" 字符. "%" 的意思是匹配零或若干字符,例如:
"%.h" 表示所有以.h结尾的文件. <pattern>指定了要搜索的文件集, 而<directories>则指定
了<pattern>的文件集的搜索的目录. 例如:

vpath %.h ../headers
该语句表示, 要求make在 "../headers" 目录下搜索所有以.h结尾的文件.

    我们可以连续地使用vpath语句, 以指定不同搜索策略. 如果连续的vpath语句中出现了相同
的<pattern>, 或是被重复了的<pattern>, make会按照vpath语句的先后顺序来执行搜索. 如:

    vpath %.c    foo
    vpath %.c    blish
    vpath %.c    bar
表示.c结尾的文件, 先在foo目录, 然后是blish, 最后是bar目录.

    vpath %.c    foo: bar
    vpath %.c    blish
表示.c结尾的文件, 先在foo目录, 然后是bar目录, 最后才是blish目录.

五. 伪目标
    
    最早先的一个例子中, 我们提到过一个clean的目标, 这是一个伪目标.

Clean:
    rm *.o temp
    象clean一样, 我们生成了许多编译文件,我们也应该提供一个清除它们的"目标"以备完整地
重编译而用, 以make clean来使用该目标.
    我们并不生成clean这个文件. "伪目标"并不是一个文件, 只是一个标签,make无法生成它的
依赖关系和决定它是否要执行. 我们只有通过显式地指明这个"目标"才能让其生效. 当然,"伪目
标" 的取名不能和文件名重名, 不然其就失去伪目标的意义了.
    为了避免和文件重名, 我们使用特殊标记.PHONY来显式地指明一个目标是"伪目标", 向make
说明, 不管是否有这个文件, 这个目标就是"伪目标".

.PHONY: clean
只要有这个声明, 不管是否有clean文件, 要运行clean这个目标, 只有make clean这样. 于是整
个过程可以这样写:

.PHONY: clean
clean:
    rm *.o temp
伪目标一般没有依赖的文件. 我们可以为伪目标指定依赖文件, 伪目标同样可以作为默认目标,
只要将其放在第一个. 一个示例就是,如果你的 Makefile需要一口气生成若干个可执行文件,但
你只想简单地敲一个make完事, 并且, 所有的目标文件都写在一个Makefile中, 那么你可以使用
伪目标这个特性.
####################################################################################
#Exampke make start
all: prog1 prog2 prog3
.PHONY : all

prog1: prog1.o utils.o
    cc -o prog1 prog1.o utils.o

prog2: prog2.o
    cc -o prog2 prog2.o

prog3: prog3.o sort.o utils.o
    cc -o prog3 prog3.o sort.o utils.o
#Example make end
####################################################################################

    Makefile中的第一个目标会被作为其默认目标. 我们声明了一个all的伪目标,其依赖于其它
三个目标. 由于伪目标的特性是: 总是被执行的,所以其依赖的那三个目标就总是不如all这个目
标新. 所以, 其它三个目标的规则总是会被执行. 也就达到了我们一口气生成多个目标的目的.
.PHONY: all声明了all这个目标为"伪目标".
    上面的例子我们可以看出, 目标也可以成为依赖. 所以, 伪目标同样也可成为依赖, 看下面
的例子:
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#Exampke make start
.PHONY: cleanall cleanobj cleandiff

cleanall:    cleanobj cleandiff
    rm program
cleanobj:
    rm *.o
cleandiff:
    rm *.diff
#Example make end
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
    make cleanall将清除所有要被清除的文件. cleanobj和cleandiff这两个伪目标有点像 "子
程序" 的意思. 我们可以输入 "make cleanall" 和 "make cleanobj" 和 "make cleandiff" 命
令来达到清除不同种类文件的目的.

六. 多目标

    Makefile的规则中的目标可以不止一个, 其支持多目标, 有可能我们的多个目标同时依赖于
一个文件, 并且其生成的命令大体类似. 于是我们就能把其合并起来. 当然, 多个目标的生成规
则的执行命令是同一个, 这可能会可我们带来麻烦, 不过好在我们的可以使用一个自动化变量$@
这个变量表示着目前规则中所有的目标的集合.

bigoutput littleoutput:    text.g
    generate text.g -$(subst output,,$@) > $@

上述规则等价于:
    
bigoutput: text.g
    generate text.g -big > bigoutput
littleoutput: text.g
    generate text.g -little > littleoutput

其中, -$(subst output,,$@)中的 "$" 表示执行一个Makefile的函数, 函数名为subst, 后面的
为参数. 关于函数, 将在后面讲述. 这里的这个函数是截取字符串的意思,"$@" 表示目标的集合
就像一个数组, "$@" 依次取出目标, 并执于命令.

七. 静态模式

    静态模式可以更加容易地定义多目标的规则, 可以让我们的规则变得更加的有弹性和灵活.
语法如下:
<targets ...>:    <target-pattern>:    <prereq-patterns ...>
    <commands>
...
说明:
    targets:            一系列的目标文件, 可以有通配符.
    target-parrtern:    targets的模式, 也就是的目标集模式.
    prereq-parrterns:    目标的依赖模式,它对target-parrtern形成的模式进行依赖目标的定
                        义。
    举个例子来说明如下: 如果我们的<target-parrtern>定义成 "%.o",意思是我们的<target>
集合中都是以.o结尾的, 如果<prereq-parrterns>定义成 "%.c",意思是对<target-parrtern>所
形成的目标集进行二次定义, 其计算方法是, 取<target-parrtern>模式中的 "%",也就是去掉了
.o这个结尾, 并为其加上.c这个结尾, 形成的新集合.
    因此, 我们的 "目标模式" 或是 "依赖模式" 中都应该有 "%" 这个字符,如果你的文件名中
有 "%" 那么你可以使用反斜杠“\”进行转义,来标明真实的“%”字符。

#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#Exampke make start
#例7-1
objects = foo.o bar.o
all: $(objects)

$(objects):    %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
#Example make end
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
例7-1中, 目标从$object中获取. "%.o" 表明所有以 ".o" 结尾的目标,即变量$object集合的模
式, 依赖模式 "%.c" 则取模式“%.o”的 "%", 也就是 "foo bar", 并为其加下.c的后缀, 于是,
我们的依赖目标就是 "foo.c bar.c". 而命令中的 "$<" 和 "$@" 则是自动化变量, "$<"表示所
有的依赖目标集(foo.c bar.c), "$@" 表示目标集(foo.o bar.o). 于是,上面的规则展开后等价
于下面的规则:
foo.o: foo.c
    $(CC) -c $(CFLAGS) foo.c -o foo.o
bar.o: bar.c
    $(CC) -c $(CFLAGS) bar.c -o bar.o
如果我们的 "%.o" 有几百个, 我们只要用这种很简单的 "静态模式规则" 就可以写完一堆规则.
"静态模式规则" 的用法很灵活, 如果用得好, 那会一个很强大的功能.
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#Exampke make start
#例7-2
files = foo.elc bar.o lose.o

$(filter %.o, $(files)): %.o: %.c
    $(CC) -c $(CFLAGS) $< -o $@
$(filter %.elc, $(files)): %.elc: %.el
    emacs -f batch-byte-compile $<
#Example make end
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
$(filter %.o, $(files))表示调用Makefile的filter函数, 过滤 "$filter" 集, 只要其中模式
为 "%.o" 的内容.????????

八. 自动生成依赖性

    在Makefile中, 我们的依赖关系可能会需要包含一系列的头文件, 比如, 如果我们的main.c
中有一句 "#include "defs.h" ", 那么我们的依赖关系应该是:
main.o: main.c defs.h
    但是, 如果是一个比较大型的工程, 你必需清楚哪些C文件包含了哪些头文件, 并且,你在加
入或删除头文件时, 也需要小心地修改Makefile, 这是一个很没有维护性的工作. 为了避免这种
繁重而又容易出错的事情, 我们可以使用C编译的一个功能. 大多数的C编译器都支持一个 "-M"
的选项, 即自动找寻源文件中包含的头文件, 并生成一个依赖关系. 例如, 如果我们执行下面的
命令:
    cc -M main.c
其输出是:
    main.o: main.c defs.h
于是由编译器自动生成的依赖关系, 这样一来, 你就不必再手动书写若干文件的依赖关系, 而由
编译器自动生成了. 需要注意的是,如果使用GNU的编译器, 需要用 "-MM" 参数, 不然, "-M"参
数会把一些标准库的头文件也包含进来.
gcc -M main.c的输出是:
    main.o: main.c defs.h /usr/include/stdio.h /usr/include/features.h \
    /usr/include/sys/cdefs.h /usr/include/gnu/stubs.h \
    /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stddef.h \
    /usr/include/bits/types.h /usr/include/bits/pthreadtypes.h \
    /usr/include/bits/sched.h /usr/include/libio.h \
    /usr/include/_G_config.h /usr/include/wchar.h \
    /usr/include/bits/wchar.h /usr/include/gconv.h \
    /usr/lib/gcc-lib/i486-suse-linux/2.95.3/include/stdarg.h \
    /usr/include/bits/stdio_lim.h
gcc -MM main.c的输出则是:
    main.o: main.c defs.h

那么, 编译器的这个功能如何与我们的Makefile联系在一起呢? 因为这样一来, 我们的Makefile
也要根据这些源文件重新生成, 让Makefile自已依赖于源文件? 这个功能并不现实, 不过我们可
以有其它手段来迂回地实现这一功能.GNU组织建议把编译器为每一个源文件的自动生成的依赖关
系放到一个文件中, 为每一个 "name.c" 的文件都生成一个 "name.d" 的Makefile文件, .d文件
中就存放对应.c文件的依赖关系.
    于是, 我们可以写出.c文件和.d文件的依赖关系, 并让make自动更新或自成.d文件, 并把其
包含在我们的主Makefile中, 这样, 我们就可以自动生成每个文件的依赖关系了.
    这里, 我们给出了一个模式规则来产生.d文件:
#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
#Exampke make start
%.d:    %.c
    @set -e; rm -f $@; \
    $(CC) -M $(CPPFLAGS) $< > $@.$$$$; \
    sed s,\($*\)\.o[ :]*,\1.o $@ : ,g < $@.$$$$ > $@; \
    rm -f $@.$$$$
#Example make end
#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
以上程序使所有的.d文件依赖于.c文件.
第一行 "rm -f $@" 的意思是删除所有的目标,也就是.d文件;
第二行的意思是, 为每个依赖文件 "$<", 也就是.c文件生成依赖文件,"$@" 表示模式 "%.d" 文
件, 如果有一个C文件是name.c, 那么 "%" 就是name, "$$$$" 意为一个随机编号,第二行生成的
文件有可能是 "name.d.12345";
第三行使用sed命令做了一个替换;
第四行就是删除临时文件.

    总而言之,这个模式要做的事就是在编译器生成的依赖关系中加入.d文件的依赖, 即把依赖
关系:
main.o: main.c defs.h
转成:
main.o main.d: main.c defs.h

    于是, 我们的.d文件也会自动更新了, 并会自动生成了, 当然, 你还可以在这个.d文件中加
入的不只是依赖关系, 包括生成的命令也可一并加入, 让每个.d文件都包含一个完赖的规则. 一
旦我们完成这个工作, 接下来, 我们就要把这些自动生成的规则放进我们的主Makefile中. 我们
可以使用Makefile的 "include" 命令, 来引入别的Makefile文件, 例如:
sources = foo.c bar.c
include $(sources:.c=.d)
上述语句中的 "$(sources:.c=.d)" 中的 ".c=.d" 的意思是做一个替换,把变量$(sources)所有
.c的字串都替换成.d, 关于这个替换的内容, 在后面我会有更为详细的讲述. 当然, 你得注意次
序, 因为include是按次来载入文件, 最先载入的.d文件中的目标会成为默认目标.


【2006-12-23】

PARTNER CONTENT

文章评论0条评论)

登录后参与讨论
我要评论
0
9
关闭 站长推荐上一条 /3 下一条