怎样写Makefile文件(C语言部分)

  本文摘抄自“跟我一起写Makefile ”,只是原文中我自己感觉比较精要的一部分,并且只针对C语言,使用GCC编译器。 原文请看这里:http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile

        写完之后才发现基本上都是一些比较枯燥的规则,看看一、二、八三个部分就可以了。当作参考工具吧,什么时候用到了再来看看。

 

一、概述

        我所使用的make 版本是 GNU Make 3.81,使用的系统是 Ubuntu 10.10,GCC版本为 4.4.5。与原文作者使用的Make 版本很相似。

 

1.1  关于程序的编译和链接

        对于C语言的编译,首先要把源文件编译成中间代码文件,即.o文件(在Windows下是.obj文件)。这个动作叫做编译compile)。然后再把大量Object File合成执行文件,这个动作叫做链接link)。更加详细的内容可以参考这里:使用gcc编译C程序的详细过程

        编译时,编译器主要检查语法、函数与变量的声明是否正确。函数的声明通常放在头文件中(头文件中应该只放声明,对于函数的具体实现则应该放到单独的 .c 源文件中),你告诉编译器头文件所在的位置,编译器就会到相应的地方去找函数声明。只要语法正确,编译器就会生成中间文件。一般来说一个源文件(.c)对应一个目标文件(.o)。

        链接时,主要是链接函数和全局变量,所以,我们可以使用这些中间目标文件(O文件或是OBJ文件)来链接我们的应用程序,并不需要源文件的存在。在很多时候,由于中间目标文件太多,而在链接时需要明显地指出中间目标文件名,这对于编译很不方便,所以,我们要给中间目标文件打个包,在Windows下这种包叫“库文件”(Library File),也就是 .lib 文件,在UNIX下,是Archive File,也就是 .a 文件

       

二、Makefile 介绍

2.1   Makefile的规则

target ... : prerequisites ...
	command
	...
	...

        target可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。

        prerequisites就是,要生成那个target所需要的文件或是目标。

        command也就是make需要执行的命令。(任意的shell命令)。注意command之前是TAB键,并非空格键。

 

2.2   一个试例

        一个工程有3个头文件,和8个c文件,关系如下图:

文件关联

        我们的makefile应该是下面的这个样子的:

edit : main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o
	cc -o edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

main.o : main.c defs.h
	cc -c main.c
kbd.o : kbd.c defs.h command.h
	cc -c kbd.c
command.o : command.c defs.h command.h
	cc -c command.c
display.o : display.c defs.h buffer.h
	cc -c display.c
insert.o : insert.c defs.h buffer.h
	cc -c insert.c
search.o : search.c defs.h buffer.h
	cc -c search.c
files.o : files.c defs.h buffer.h command.h
	cc -c files.c
utils.o : utils.c defs.h
	cc -c utils.c
clean :
	rm edit main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

        make命令会找到Makefile文件中第一个target,并且把它当做最终的目标文件。Makefile所指示出来的关系就是我们在上面的图中所展示的关系。

 

2.3   Makefile中使用变量

        makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o

edit : $(objects)
	cc -o edit $(objects)

 

 

2.4  让make自动推导

        只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中。

objects = main.o kbd.o command.o display.o \
		insert.o search.o files.o utils.o
 cc = gcc

edit : $(objects)
	cc -o edit $(objects)

main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h

.PHONY : clean
clean :
	rm edit $(objects)
“.PHONY”表示,clean是个伪目标文件。

 

2.5   清空目标文件的规则

.PHONY : clean
clean :
	-rm edit $(objects)
在rm命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然,clean的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。

 

2.6   Makefile中有什么?

Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释。

  1. 显式规则。显式规则说明了,如何生成一个或多的的目标文件。这是由Makefile的书写者明显指出,要生成的文件,文件的依赖文件,生成的命令。
  2. 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写Makefile,这是由make所支持的。
  3. 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
  4. 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
  5. 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用“#”字符,这个就像C/C++中的“//”一样。如果你要在你的Makefile中使用“#”字符,可以用反斜框进行转义,如:“\#”。

最后,还值得一提的是,在Makefile中的命令,必须要以[Tab]键开始。

 

2.7   引用其他Makefile

        在include前面可以有一些空字符,但是绝不能是[Tab]键开始。你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar),其包含了 e.mk和f.mk。

include foo.make *.mk $(bar)

如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:

  1. 如果make执行时,有“-I”或“--include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录<prefix>;/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。

如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取, make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”,其表示,无论include过程中出现什么错误,都不要报错继续执行。


2.8   make的工作方式

GNU的make工作时的执行步骤入下:(想来其它的make也是类似)

  1. 读入所有的Makefile。
  2. 读入被include的其它Makefile。
  3. 初始化文件中的变量。
  4. 推导隐晦规则,并分析所有规则。
  5. 为所有的目标文件创建依赖关系链。
  6. 根据依赖关系,决定哪些目标要重新生成。
  7. 执行生成命令。

1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。

 

        看完这些再看 第八部分:隐含规则,就可以了,中间的都不用看了。我怎么写了这么多。

 

三、书写规则

3.1   文件搜索

        使用特殊变量VPATH 来搜索,如果不指定,make只会在当前的目录中去找寻依赖文件和目标文件。指定了这个变量,在当前目录搜索完而找不到之后会搜索相应目录。

VPATH = src:../headers

        目录之间用冒号隔开。

        还可以使用vpath关键字,它的使用方法有三种:这里只说一种,详情看原文

1、vpath <pattern> <directories>

为符合模式<pattern>的文件指定搜索目录<directories>。

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

vpath %.h ../headers

 

3.2   伪目标
        比如刚开始我们指定的

.PHONY : clean
clean :
	rm *.o temp

        也可以为伪目标指定依赖文件,常见于all,并且把它放在最前,指定为Makefile的终极目标。由于all是伪目标,所以不会生成all文件。

all : prog1 prog2 prog3
.PHONY : all

 

3.3   静态模式
        静态模式可以更加容易地定义多目标的规则,语法:

<targets ...>: <target-pattern>: <prereq-patterns ...>
	<commands>
	...


        如下面的例子:

objects = foo.o bar.o

all: $(objects)

$(objects): %.o: %.c
	$(CC) -c $(CFLAGS) $< -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


        $(objects): %.o: %.c,这里先把$(object) 在这里展开,表示把所有objects之中的.o文件都用.c文件与之对应起来。

        $< 表示依赖文件,这里即.c文件, $@ 表示target文件,这里即.o文件。

 

3.4   自动生成依赖性

        在GCC中,我们用gcc –MM main.c 这条命令就可以查看所有main.o 的依赖关系。于是,可以利用这一特性来让Makefile自动生成依赖关系,不用我们自己写了。

        具体看这里

 

四、书写命令

 

 

 

 

 

 

 

        每条规则中的命令和操作系统Shell的命令行是一致的。make会按顺序一条一条的执行命令,每条命令的开头必须以[Tab]键开头,除非,命令是紧跟在依赖规则后面的分号后的。

4.1   显示命令

        用“@”字符在命令行前,那么,这个命令将不被make显示出来。

        make执行时,带入make参数“-n”或“--just-print”,那么其只是显示命令,但不会执行命令。利于调试Makefile。

        make参数“-s”或“--slient”则是全面禁止命令的显示。

 

4.2   命令执行

        如果上一条命令执行的结果需要在下一条指令中使用,那么这两个指令应该放在一行,中间用分号隔开。

exec:
	cd /home/hchen; pwd

 

4.3   命令出错

        如果想要忽略命令的错误,在它之前加 ‘-’即可。

 

4.4   嵌套执行make

        在一些大的工程中,我们会把我们不同模块或是不同功能的源文件放在不同的目录中,我们可以在每个目录中都书写一个该目录的Makefile,这有利于让我们的Makefile变得更加地简洁,而不至于把所有的东西全部写在一个Makefile中,这样会很难维护我们的Makefile,这个技术对于我们模块编译和分段编译有着非常大的好处。

        例如,我们有一个子目录叫subdir,这个目录下有个Makefile文件,来指明了这个目录下文件的编译规则。那么我们总控的Makefile可以这样书写:

subsystem:
        cd subdir && $(MAKE)

        等价于:

subsystem:
        $(MAKE) -C subdir

        具体看这里

 

五、使用变量

        在Makefile中的定义的变量,代表了一个文本字串,在Makefile中执行的时候其会自动原模原样地展开在所使用的地方。变量是大小写敏感的。“$<”、“$@”等,这些是自动化变量。

5.1   变量的基础

        变量在定义时要赋初值。使用时在其前加$ 符号,并且用() 或者 {} 把变量名括起来。"$$” 表示真实的 $。

 

5.2   变量中的变量

        这里说变量怎样定义,怎样赋值。

        变量赋值有三种方式,一种是 = ,一种是 := ,一种是 ?= 。前一种可以使用延后定义的变量,中间只能按照从上到下的顺序定义变量。最后一种表示如果这个变量已经赋值,就不再对他进行赋值了,如果没有赋值,就对他赋值。详情参看这里

        += 是追加赋值。

        比如定义一个空格,下面的变量space就表示一个空格,其他方法还不好定义空格:

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

 

 

5.3   变量高级用法

      · 变量值的替换:
        我们可以替换变量中的共有的部分,其格式是“$(var:a=b)”或是“${var:a=b}”,其意思是,把变量“var”中所有以“a”字串“结尾”的“a”替换成“b”字串。这里的“结尾”意思是“空格”或是“结束符”。例如:

foo := a.o b.o c.o
bar := $(foo:.o=.c)

        这个示例中,我们先定义了一个“$(foo)”变量,而第二行的意思是把“$(foo)”中所有以“.o”字串“结尾”全部替换成“.c”,所以我们的“$(bar)”的值就是“a.c b.c c.c”。

对于静态模式,我们要这样写:   bar := $(foo:%.o=%.c)

      · 把变量的值再当成变量

        即使用变量时,每个变量都展开成一个字符串使用:

x = y
y = z
a := $($(x))

        在这个例子中,$(x)的值是“y”,所以$($(x))就是$(y),于是$(a)的值就是“z”。(注意,是“x=y”,而不是“x=$(y)”)

        更加详细的描述见这里

 

5.4   目标变量

        为某个target指定只会在这条规则以及它所连带的规则中使用的变量:

prog : CFLAGS = -g
prog : prog.o foo.o bar.o
        $(CC) $(CFLAGS) prog.o foo.o bar.o

prog.o : prog.c
        $(CC) $(CFLAGS) prog.c

foo.o : foo.c
        $(CC) $(CFLAGS) foo.c

bar.o : bar.c
        $(CC) $(CFLAGS) bar.c

        在这个示例中,不管全局的$(CFLAGS)的值是什么,在prog目标,以及其所引发的所有规则中(prog.o foo.o bar.o的规则),$(CFLAGS)的值都是“-g”

 

六、使用函数

        在Makefile中可以使用函数来处理变量,从而让我们的命令或是规则更为的灵活和具有智能。make所支持的函数也不算很多,不过已经足够我们的操作了。函数调用后,函数的返回值可以当做变量来使用。

6.1   函数调用的语法

        函数调用,很像变量的使用,也是以“$”来标识的,其語法如下:

$(<function> <arguments>)
或者
${<function> <arguments>}


        <arguments>为函数的参数,参数间以逗号“,”分隔,而函数名和参数之间以“空格”分隔。下面是一个具体的例子:

comma:= ,
empty:=
space:= $(empty) $(empty)
foo:= a b c
bar:= $(subst $(space),$(comma),$(foo))

        在这个示例中,$(comma)的值是一个逗号。$(space)使用了$(empty)定义了一个空格,$(foo)的值是“a b c”,$ (bar)的定义用,调用了函数“subst”,$(bar)的值是“a,b,c”。

 

6.2   字符串处理函数

$(subst <from>,<to>,<text>) 

  • 名称:字符串替换函数——subst。
  • 功能:把字串<text>中的<from>字符串替换成<to>。
  • 返回:函数返回被替换过后的字符串。

 

$(patsubst <pattern>,<replacement>,<text>) 

  • 名称:模式字符串替换函数——patsubst。
  • 功能:查找<text>中的单词(单词以“空格”、“Tab”或“回车”“换行”分隔)是否符合模式< pattern>,如果匹配的话,则以<replacement>替换。这里,<pattern>可以包括通配符 “%”,表示任意长度的字串。如果<replacement>中也包含“%”,那么,<replacement>中的这个 “%”将是<pattern>中的那个“%”所代表的字串。(可以用“\”来转义,以“\%”来表示真实含义的“%”字符)
  • 返回:函数返回被替换过后的字符串。

        这和我们前面“变量章节”说过的相关知识有点相似。如:

        “$(var:<pattern>=<replacement>;)” 相当于 “$(patsubst <pattern>,<replacement>,$(var))”,

        而“$(var: <suffix>=<replacement>)” 则相当于 “$(patsubst %<suffix>,%<replacement>,$(var))”。

        例如有:objects = foo.o bar.o baz.o, 那么,“$(objects:.o=.c)”和“$(patsubst %.o,%.c,$(objects))”是一样的。

 

$(filter <pattern...>,<text>)

  • 名称:过滤函数——filter。
  • 功能:以<pattern>模式过滤<text>字符串中的单词,保留符合模式<pattern>的单词。可以有多个模式。
  • 返回:返回符合模式<pattern>;的字串。

        其他更多的函数和示例参看这里

 

6.3   文件名操作函数

        这里列出很多对文件名的操作函数。

        还有foreach, if, call这三个比较特殊的函数。

 

七、make的运行

7.1   指定Makefile

        指定某个不叫Makefile的文件运行make命令,加-f选项:

make –f hchen.mk

 

7.2   指定目标

 

      “all”这个伪目标是所有目标的目标,其功能一般是编译所有的目标。“clean”这个伪目标功能是删除所有被make创建的文件。“install”这个伪目标功能是安装已编译好的程序,其实就是把目标执行文件拷贝到指定的目标中去。“print”这个伪目标的功能是例出改变过的源文件。“tar”这个伪目标功能是把源程序打包备份。也就是一个tar文件。“dist”这个伪目标功能是创建一个压缩文件,一般是把tar文件压成Z文件。或是gz文件。“TAGS”这个伪目标功能是更新所有的目标,以备完整地重编译使用。“check”和“test”这两个伪目标一般用来测试makefile的流程。

       

7.3   检查规则

        “-n” “--just-print” “--dry-run” “--recon” 不执行参数,这些参数只是打印命令,不管目标是否更新,把规则和连带规则下的命令打印出来,但不执行,这些参数对于我们调试makefile很有用处。

 

7.4   make的参数

        点击小标题。

 

八、隐含规则

8.1   C语言编译的隐含规则

        “<n>;.o”的目标的依赖目标会自动推导为“<n>;.c”,并且其生成命令是“$(CC) –c $(CPPFLAGS) $(CFLAGS)”。

        其他语言的隐含规则看这里

 

8.2   隐含规则使用的变量

        1、关于命令的变量

 

             CCC语言编译程序。默认命令是“cc”。

        2、关于命令参数的变量

 

             CFLAGSC语言编译器参数。

        更多变量看这里

 

8.3   模式规则

 

 

 

 

 

 

 

 

 

        模式规则中都是用%,它表示匹配0个(还是1个?)或多个字符。

   %.o : %.c
           $(CC) -c $(CFLAGS) $(CPPFLAGS) $< -o $@


 

8.4   自动化变量

        所谓自动化变量,就是这种变量会把模式中所定义的一系列的文件自动地挨个取出,直至所有的符合模式的文件都取完了。这种自动化变量只应出现在模式规则的命令中。

        在  a: a.o b.o c.o 这个依赖规则中,a 叫做target,即目标; a.o b.o c.o 都叫做 依赖目标。

 

$@
表示规则中的目标文件集(target,比如上面例子中的%.o)。在模式规则中,如果有多个目标,那么,"$@"就是匹配于目标中模式定义的集合。
$%
仅当目标是函数库文件中,表示规则中的目标成员名。例如,如果一个目标是"foo.a(bar.o)",那么,"$%"就是 "bar.o","$@"就是"foo.a"。如果目标不是函数库文件(Unix下是[.a],Windows下是[.lib]),那么,其值为空。
$< 
依赖目标(比如上例种的%.c)中的第一个目标名字。如果依赖目标是以模式(即"%")定义的,那么"$<"将是符合模式的一系列的文件集。注意,其是一个一个取出来的。
$?
所有比目标新的 依赖目标 的集合。以空格分隔。
$^
所有的 依赖目标 的集合。以空格分隔。如果在依赖目标中有多个重复的,那个这个变量会去除重复的依赖目标,只保留一份。
$+
这个变量很像"$^",也是所有依赖目标的集合。只是它不去除重复的依赖目标。
$*
这个变量表示目标模式中"%"及其之前的部分。如果目标是"dir/a.foo.b",并且目标的模式是"a.%.b",那么,"$*"的值就是"dir/a.foo"。

 

 

 

多么希望有个实例可以做一下呀,有好多文件,看看怎么把这些文件用Makefile组织起来,并且优化Makefile的文件。只看这些干巴巴的规则,一会儿就累了………………………………

 
 
posted @ 2016-12-09 13:59  蓝夜  阅读(301)  评论(0编辑  收藏  举报