提炼《跟我一起写 Makefile》教程
提炼《跟我一起写 Makefile》教程
支持原创,请移步原作者博客:
一、makefile总序
1. makefile文件格式
1 | target...:prerequisites... |
target: 是一个目标文件,也可以是执行文件。还可以是一个标签。(Label)
prerequisites: 依赖项,就是要生成那个target所需要的文件或是目标。
command: make需要执行的命令。(任意的Shell命令,必须以Tab开头)
2. makefile执行原理
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
3. 定义、使用变量
1 | objects = main.o kbd.o command.o display.o \ # 定义变量(类似c/c++中的宏) |
4. make自动推导
只要make看到一个[.o]文件(目标文件是.o文件),它就会自动的把[.c]文件加在依赖关系中,所以依赖中可以省略。
1 | objects = main.o kbd.o command.o display.o \ |
不用自动推导前写法:
1 | objects = main.o kbd.o command.o display.o \ |
注意在Makefile中的命令,必须要以[Tab]键开始。
5. 引用其它的makefile文件
在Makefile使用include关键字可以把别的Makefile包含进来,这很像C语言的#include,被包含的文件会原模原样的放在当前文件的包含位置。
在include前面可以有一些空字符,但是绝不能是[Tab]键开始。include和可以用一个或多个空格隔开。举个例子,你有这样几个Makefile:a.mk、b.mk、c.mk,还有一个文件叫foo.make,以及一个变量$(bar),其包含了e.mk和f.mk,那么,下面的语句:
include foo.make *.mk $(bar)
等价于:
include foo.make a.mk b.mk c.mk e.mk f.mk
make命令开始时,会查找include所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的#include指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
1
2 1.如果make执行时,有“-I”或“--include-dir”参数,那么make就会在这个参数所指定的目录下去寻找。
2.如果目录/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。
如果有文件没有找到的话,make会生成一条警告信息,但不会马上出现致命错误。它会继续载入其它的文件,一旦完成makefile的读取,make会再重试这些没有找到,或是不能读取的文件,如果还是不行,make才会出现一条致命信息。如果你想让make不理那些无法读取的文件,而继续执行,你可以在include前加一个减号“-”。如:
1 | -include<filename> |
其表示,无论include过程中出现什么错误,都不要报错继续执行。和其它版本make兼容的相关命令是sinclude,其作用和这一个是一样的。
6. make的工作方式
GNU的make工作时的执行步骤入下:(想来其它的make也是类似)
读入所有的Makefile。(全局环境变量MAKEFILES的值做include操作)
读入被include的其它Makefile。
初始化文件中的变量。
推导隐晦规则,并分析所有规则。
为所有的目标文件创建依赖关系链。
根据依赖关系,决定哪些目标要重新生成。
执行生成命令。
1-5步为第一个阶段,6-7为第二个阶段。第一个阶段中,如果定义的变量被使用了,那么,make会把其展开在使用的位置。但make并不会完全马上展开,make使用的是拖延战术,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开。
二、makefile常用规则
1. 指定依赖文件搜索目录
首先在当前目录查找,找不到会去VPATH变量中找。例如:
1 | VPATH = src:../headers |
上面的的定义指定两个目录,“src”和“../headers”,make会按照这个顺序进行搜索。目录由“冒号”分隔。(当然,当前目录永远是最高优先搜索的地方)
另外还一个小写vpath,这不是变量,这是一个make的关键字,这和上面提到的那个VPATH变量很类似,但是它更为灵活。它可以指定不同的文件在不同的搜索目录中。
它的使用方法有三种:
vpath < pattern> < directories> 为符合模式< pattern>的文件指定搜索目录
。 vpath < pattern> 清除符合模式< pattern>的文件的搜索目录。
vpath 清除所有已被设置好了的文件搜索目录。
vapth使用方法中的< pattern>需要包含“%”字符。“%”的意思是匹配零或若干字符,例如,“%.h”表示所有以“.h”结尾的文件。< pattern>指定了要搜索的文件集,而< directories>则指定了的文件集的搜索的目录。例如:
vpath %.h ../headers
该语句表示,要求make在“../headers”目录下搜索所有以“.h”结尾的文件。(如果某文件在当前目录没有找到的话)
2. 伪目标
前面示例中最后的clean就是一个伪目标,“伪目标”并不是一个文件,只是一个标签,不会像其它目标一样生成“clean”这个文件。所以伪目标下的命令执行,我们只有通过显示地指明这个“目标”才能让其生效(make clean)。
当然,“伪目标”的取名不能和其它目标文件重名,不然其就失去了“伪目标”的意义了。为了避免这种情况,我们可以使用一个特殊的标记“.PHONY”来显示地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
伪目标常规使用
1
2
3
clean:
rm *.o temp如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中
1
2
3
4
5
6
7
8
9
10
11
12
13all : 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
我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于伪目标的特性是,总是被执行的,所以其依赖的那三个目标就总是不如“all”这个目标新。所以,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。“.PHONY : all”声明了“all”这个目标为“伪目标”。从上面的例子我们可以看出,目标也可以成为依赖。所以,伪目标同样也可成为依赖
1
2
3
4
5
6
7
8
9
cleanall : cleanobj cleandiff
rm program
cleanobj :
rm *.o
cleandiff :
rm *.diff
3. 静态模式
静态模式可以更加容易地定义多目标的规则(如果我们的“%.o”有几百个),可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:
1 | <targets...>: <target-pattern>: <prereq-patterns ...> |
targets 定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-parrtern 是指明了targets的模式,也就是的目标集模式。
prereq-parrterns 是目标的依赖模式,它对target-parrtern形成的模式再进行一次依赖目标的定义。
示例一:
1 | objects = foo.o bar.o |
4. 自动生成依赖性
在 Makefile 中, 我们的依赖关系可能会需要包含一系列的头文件,如果是一个比较大型的工程,你必需清楚哪些 C 文件包含了哪些头文件,并且,你在加入或删除头文件时,也需要小心地修改 Makefile,这是一个很没有维护性的工作。
为了避免这种繁重而又容易出错的事情,我们可以使用C/C++编译的一个功能。大多数的C/C++编译器都支持一个“-M”的选项,即自动找寻源文件中包含的头文件,并生成一个依赖关系。
注意如果你使用GNU的C/C++编译器,你得用“-MM”参数,不然,“-M”参数会把一些标准库的头文件也包含进来。
- 自动生成源文件(xxx.c)对应的依赖关系文件(xxx.d)的makefile
1 | %.d: %.c |
- 在主makefile中引入上面自动生成的依赖关系makefile(也就是.d文件)
1 | sources = foo.c bar.c |
5. 定义命令包
把多条命令定义成一个变量,后续可以使用这个变量来替代一组命令。语法以define开始,以endef结束,如:
1 | define run-yacc #命令包的名字 |
三、使用变量
1. 定义变量几种形式
1 | # 1. 简单的使用“=”号,在“=”左侧是变量,右侧是变量的值,不关心变量定义顺序。 |
四、条件判断
下面的例子,判断$(CC)变量是否“gcc”,如果是的话,则使用GNU函数编译目标。
1 | libs_for_gcc = -lgnu |
条件表达式的语法为:
1 | <conditional-directive> |
五、使用函数
1. 函数使用
$(<function> <arguments> )
这里,
示例:把空格替换为逗号
1 | comma:= , |
2. 字符串处理函数
1 | $(subst <from>,<to>,<text> ) |
1 | $(patsubst <pattern>,<replacement>,<text> ) |
1 | $(strip <string> ) |
1 | $(findstring <find>,<in> ) |
1 | $(filter <pattern...>,<text> ) |
1 | $(filter-out <pattern...>,<text> ) |
1 | $(sort <list> ) |
1 | $(word <n>,<text> ) |
1 | $(wordlist <s>,<e>,<text> ) |
1 | $(words <text> ) |
1 | $(firstword <text> ) |
3. 文件名操作函数
1 | $(dir <names...> ) |
1 | $(notdir <names...> ) |
1 | $(suffix <names...> ) |
1 | $(basename <names...> ) |
1 | $(addsuffix <suffix>,<names...> ) |
1 | $(addprefix <prefix>,<names...> ) |
1 | $(join <list1>,<list2> ) |
4. foreach函数
格式:$(foreach <var>,<list>,<text> )
这个函数的意思是,把参数中的单词逐一取出放到参数所指定的变量中,然后再执行
示例:
1 | names := a b c d |
5. if 函数
格式:
1 | $(if <condition>,<then-part> ) |
结合参考:《四、条件判断》
6. call 函数
call函数是唯一一个可以用来创建新的参数化的函数。你可以写一个非常复杂的表达式,这个表达式中,你可以定义许多参数,然后你可以用call函数来向这个表达式传递参数。其语法是:
$(call <expression>,<parm1>,<parm2>,<parm3>...)
当 make执行这个函数时,
reverse = $(1) $(2)
foo = $(call reverse,a,b)
那么,foo的值就是“a b”。当然,参数的次序是可以自定义的,不一定是顺序的,如:
reverse = $(2) $(1)
foo = $(call reverse,a,b)
此时的foo的值就是“b a”。
7. origin 函数
格式:$(origin <variable> )
告诉你你的这个变量是哪里来的。
其所有返回值情况:
- “undefined”
从来没有定义过 - “default”
是一个默认的定义,比如“CC”这个变量 - “environment”
是一个环境变量,并且当Makefile被执行时,“-e”参数没有被打开 - “file”
这个变量被定义在Makefile中 - “command line”
这个变量是被命令行定义的 - “override”
是被override指示符重新定义的 - “automatic”
是一个命令运行中的自动化变量
8. shell 函数
shell函数把执行操作系统命令后的输出作为函数返回。
注意,这个函数会新生成一个Shell程序来执行命令,所以你要注意其运行性能,如果你的Makefile中有一些比较复杂的规则,并大量使用了这个函数,那么对于你的系统性能是有害的。特别是Makefile的隐晦的规则可能会让你的shell函数执行的次数比你想像的多得多。
9. 控制make的函数
调试信息,类似error、warning日志输出。
$(error <text ...> )
$(warning <text ...> )
六、如何使用make命令
一般来说,最简单的就是直接在命令行下输入make命令,make命令会找当前目录的makefile来执行,一切都是自动的。但也有时你也许只想让 make重编译某些文件,而不是整个工程,而又有的时候你有几套编译规则,你想在不同的时候使用不同的编译规则,等等。本章节就是讲述如何使用make命令的。
1. make的返回值
1 | 0 —— 表示成功执行。 |
2. 指定makefile
前面我们说过,GNU make找寻默认的Makefile的规则是在当前目录下依次找三个文件——“GNUmakefile”、“makefile”和“Makefile”。其按顺序找这三个文件,一旦找到,就
开始读取这个文件并执行。
我们也可以给make命令指定一个特殊名字的Makefile。要达到这个功能,我们要使用make的“-f”或是“–file”参数(“– makefile”参数也行)。例如,我们有个makefile的名字是“hchen.mk”,那么,我们可以这样来让make来执行这个文件:
make –f hchen.mk
3. 指定目标
一般来说,make的最终目标是makefile中的第一个目标,而其它目标一般是由这个目标连带出来的。这是make的默认行为。当然,一般来说,你的 makefile中的第一个目标是由许多个目标组成,你可以指示make,让其完成你所指定的目标。要达到这一目的很简单,在make命令后直接跟目标的名字就可以完成。
常见的指定目标操作:
1 | “all” 这个伪目标是所有目标的目标,其功能一般是编译所有的目标。 |
4. 检查规则
1 | 有时候,我们不想让我们的makefile中的规则执行起来,我们只想检查一下我们的命令,或是执行的序列。于是我们可以使用make命令的下述参数: |
5. make 参数
1 | 下面列举了所有GNU make 3.80版的参数定义。其它版本和产商的make大同小异,不过其它产商的make的具体参数还是请参考各自的产品文档。 |
七、隐含规则
1. 使用隐含规则
1 | foo : foo.o bar.o |
我们可以注意到,这个Makefile中并没有写下如何生成foo.o和bar.o这两目标的规则和命令。因为make的“隐含规则”功能会自动为我们自动去推导这两个目标的依赖目标和生成命令。
2. 常用的隐含规则
1、编译C程序的隐含规则。
“
2、编译C++程序的隐含规则。
“
不是“.C”)
3、编译Pascal程序的隐含规则。
“
4、编译Fortran/Ratfor程序的隐含规则。
“
“.f” “$(FC) –c $(FFLAGS)”
“.F” “$(FC) –c $(FFLAGS) $(CPPFLAGS)”
“.f” “$(FC) –c $(FFLAGS) $(RFLAGS)”
5、预处理Fortran/Ratfor程序的隐含规则。
“
“.F” “$(FC) –F $(CPPFLAGS) $(FFLAGS)”
“.r” “$(FC) –F $(FFLAGS) $(RFLAGS)”
6、编译Modula-2程序的隐含规则。
“
并且其生成命令是:“$(M2C) $(M2FLAGS) $(MODFLAGS)”。
7、汇编和汇编预处理的隐含规则。
“
,默认使用C预编译器“cpp”,并且其生成命令是:“$(AS) $(ASFLAGS)”。
8、链接Object文件的隐含规则。
“
于只有一个源文件的工程有效,同时也对多个Object文件(由不同的源文件生成)的也有效。例如如下规则:
x : y.o z.o
并且“x.c”、“y.c”和“z.c”都存在时,隐含规则将执行如下命令:
cc -c x.c -o x.o
cc -c y.c -o y.o
cc -c z.c -o z.o
cc x.o y.o z.o -o x
rm -f x.o
rm -f y.o
rm -f z.o
如果没有一个源文件(如上例中的x.c)和你的目标名字(如上例中的x)相关联,那么,你最好写出自己的生成规则,不然,隐含规则会报错的。
9、Yacc C程序时的隐含规则。
“
10、Lex C程序时的隐含规则。
“
11、Lex Ratfor程序时的隐含规则。
“
) $(LFALGS)”。
12、从C程序、Yacc文件或Lex文件创建Lint库的隐含规则。
“
3. 隐含规则使用的变量
在隐含规则中的命令中,基本上都是使用了一些预先设置的变量。你可以在你的makefile中改变这些变量的值,或是在make的命令行中传入这些值,或是在你的环境变量中设置这些值,无论怎么样,只要设置了这些特定的变量,那么其就会对隐含规则起作用。当然,你也可以利用make的“-R”或“–no– builtin-variables”参数来取消你所定义的变量对隐含规则的作用。
我们可以把隐含规则中使用的变量分成两种:一种是命令相关的,如“CC”;一种是参数
相的关,如“CFLAGS”。下面是所有隐含规则中会用到的变量:
1、关于命令的变量。
AR 函数库打包程序。默认命令是“ar”。
AS
汇编语言编译程序。默认命令是“as”。
CC
C语言编译程序。默认命令是“cc”。
CXX
C++语言编译程序。默认命令是“g++”。
CO
从 RCS文件中扩展文件程序。默认命令是“co”。
CPP
C程序的预处理器(输出是标准输出设备)。默认命令是“$(CC) –E”。
FC
Fortran 和 Ratfor 的编译器和预处理程序。默认命令是“f77”。
GET
从SCCS文件中扩展文件的程序。默认命令是“get”。
LEX
Lex方法分析器程序(针对于C或Ratfor)。默认命令是“lex”。
PC
Pascal语言编译程序。默认命令是“pc”。
YACC
Yacc文法分析器(针对于C程序)。默认命令是“yacc”。
YACCR
Yacc文法分析器(针对于Ratfor程序)。默认命令是“yacc –r”。
MAKEINFO
转换Texinfo源文件(.texi)到Info文件程序。默认命令是“makeinfo”。
TEX
从TeX源文件创建TeX DVI文件的程序。默认命令是“tex”。
TEXI2DVI
从Texinfo源文件创建军TeX DVI 文件的程序。默认命令是“texi2dvi”。
WEAVE
转换Web到TeX的程序。默认命令是“weave”。
CWEAVE
转换C Web 到 TeX的程序。默认命令是“cweave”。
TANGLE
转换Web到Pascal语言的程序。默认命令是“tangle”。
CTANGLE
转换C Web 到 C。默认命令是“ctangle”。
RM
删除文件命令。默认命令是“rm –f”。
2、关于命令参数的变量
下面的这些变量都是相关上面的命令的参数。如果没有指明其默认值,那么其默认值都是
空。
ARFLAGS
函数库打包程序AR命令的参数。默认值是“rv”。
ASFLAGS
汇编语言编译器参数。(当明显地调用“.s”或“.S”文件时)。
CFLAGS
C语言编译器参数。
CXXFLAGS
C++语言编译器参数。
COFLAGS
RCS命令参数。
CPPFLAGS
C预处理器参数。( C 和 Fortran 编译器也会用到)。
FFLAGS
Fortran语言编译器参数。
GFLAGS
SCCS “get”程序参数。
LDFLAGS
链接器参数。(如:“ld”)
LFLAGS
Lex文法分析器参数。
PFLAGS
Pascal语言编译器参数。
RFLAGS
Ratfor 程序的Fortran 编译器参数。
YFLAGS
Yacc文法分析器参数。
八、make其它使用
对于上述所有的make的细节,我们不但可以利用make这个工具来编译我们的程序,还可以利用make来完成其它的工作,因为规则中的命令可以是任何Shell之下的命令,所以,在Unix下,你不一定只是使用程序语言的编译器,你还可以在Makefile中书写其它的命令,如:tar、awk、mail、sed、cvs、compress、ls、rm、yacc、rpm、 ftp……等等,等等,来完成诸如**”程序打包”、”程序备份”、”制作程序安装包”、”提交代码”、”使用程序模板”、”合并文件”**等等五花八门的功能,文件操作,文件管理,编程开发设计,或是其它一些异想天开的东西。