学习makefile
先给个我的makefile学习链接
常用命令
make -j$(nproc) 概述
学习makefile是关于程序编译的必经之路,对于C++来说,编译的过程是这样的,先将C++的代码编译成object 中间文件,在windows是.obj,Linux是.o文件,这就是编译,有了中间文件后,把中间文件整合到一起,变成可执行文件,这个过程叫链接。这个过程涉及到程序的全局变量,函数声明,语言语法语义正确性检查之类的,在正常情况来说,每个cpp文件都会有对应的.o文件,对于这些来说,就需要链接去找寻这些变量是否定义,函数是否声明,语法语义是否正确。但是因为在大型项目文件太多,为了方便起见,就会给中间文件打个包,这就是库文件,在windows是.lib library 文件,Linux是.a archive 文件
makefile就定义了项目如何编译,怎么链接程序,文件编译的先后顺序,是否需要重新编译等,简化了编译的过程,makefile写得好,一个make指令就可以编译完成。
makefile规则
粗略地看一看makefile的规则
target ... : prerequisites ...
command
...
...- target:可以是一个object file(目标文件),也可以是一个执行文件,还可以是一个标签(label)。
- prerequisites:生成该target所依赖的文件和/或target
- command:该target要执行的命令(任意的shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:
prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。
这就是makefile的规则,也就是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就可以生成可执行文件edit,执行make clean就可以删除中间目标文件及可执行文件edit。
每一个 .o 文件都有一组依赖文件,而这些 .o 文件又是执行文件 edit 的依赖文件。依赖关系的实质就是说明了目标文件是由哪些文件生成的,换言之,目标文件是哪些文件更新的。
在定义好依赖关系后,后续的那一行定义了如何生成目标文件的操作系统命令,一定要以一个 Tab 键作为开头。记住,make并不管命令是怎么工作的,他只管执行所定义的命令。make会比较targets文件和prerequisites文件的修改日期,如果prerequisites文件的日期要比targets文件的日期要新,或者target不存在的话,那么,make就会执行后续定义的命令。
clean 不是一个文件,它只不过是一个动作名字,其冒号后什么也没有,那么,make就不会自动去找它的依赖性,也就不会自动执行其后所定义的命令。要执行其后的命令,就要在make命令后明显得指出这个label的名字。
makefile过程
在默认的方式下,也就是我们只输入 make 命令。那么,
- make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
- 如果edit文件不存在,或是edit所依赖的后面的
.o文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成edit这个文件。 - 如果
edit所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程) - 当然,你的C文件和H文件是存在的啦,于是make会生成
.o文件,然后再用.o文件生成make的终极任务,也就是执行文件edit了。
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我们可以看到 .o 文件的字符串被重复了两次,如果我们的工程需要加入一个新的 .o 文件,就需要手动更改两个地方,如果涉及的地方一多,改起来就很麻烦,还容易出错。所以引入变量的概念。
变量定义
定义objects变量
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o定义后,就可以如此使用$(Var)
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)makefile的自动推导
其实makefile默认会将.o同名的.c文件进行链接,加入到依赖关系中,cc -c *.c也就自动添加进命令中执行编译.c文件了,所以也就不用每个都写cc -c *.c了
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
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是伪目标文件
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
cc -o edit $(objects)
$(objects) : defs.h
kbd.o command.o files.o : command.h
display.o insert.o search.o files.o : buffer.h
.PHONY : clean
clean :
rm edit $(objects)这是新风格makefile,以.h文件为主,看每个.h文件被哪些.o文件依赖,传统风格是以.o文件为主,看每个.o文件依赖哪些.h文件,其实我也觉得传统风格比较清晰。
每个Makefile中都应该写一个清空目标文件( .o 和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。一般的风格都是:
clean:
rm edit $(objects)更为稳健的做法是:
.PHONY : clean
clean :
-rm edit $(objects)前面说过, .PHONY 表示 clean 是一个“伪目标”。而在 rm 命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean 的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。
伪目标
“伪目标”的取名不能和文件名重名,不然其就失去了“伪目标”的意义了。
当然,为了避免和文件重名的这种情况,我们可以使用一个特殊的标记“.PHONY”来显式地指明一个目标是“伪目标”,向make说明,不管是否有这个文件,这个目标就是“伪目标”。
伪目标一般没有依赖的文件。但是,我们也可以为伪目标指定所依赖的文件。伪目标同样可以作为“默认目标”,只要将其放在第一个。
一个示例就是,如果你的Makefile需要一口气生成若干个可执行文件,但你只想简单地敲一个make完事,并且,所有的目标文件都写在一个Makefile中,那么你可以使用“伪目标”这个特性:
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我们知道,Makefile中的第一个目标会被作为其默认目标。我们声明了一个“all”的伪目标,其依赖于其它三个目标。由于默认目标的特性是,总是被执行的,但由于“all”又是一个伪目标,伪目标只是一个标签不会生成文件,所以不会有“all”文件产生。于是,其它三个目标的规则总是会被决议。也就达到了我们一口气生成多个目标的目的。 .PHONY : all 声明了“all”这个目标为“伪目标”。
makefile里有什么
Makefile里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
- 隐晦规则。由于我们的make有自动推导的功能,所以隐晦的规则可以让我们比较简略地书写 Makefile,这是由make所支持的。
- 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
- 文件指示。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
- 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用
#字符,这个就像C/C++中的//一样。如果你要在你的Makefile中使用#字符,可以用反斜杠进行转义,如:\#。
最后,还值得一提的是,在Makefile中的命令,必须要以 Tab 键开始。
makefile的文件名
默认的情况下,make命令会在当前目录下按顺序找寻文件名为“GNUmakefile”、“makefile”、“Makefile”的文件,找到了解释这个文件。在这三个文件名中,最好使用“Makefile”这个文件名,因为,这个文件名第一个字符为大写,这样有一种显目的感觉。最好不要用“GNUmakefile”,这个文件是GNU的make识别的。有另外一些make只对全小写的“makefile”文件名敏感,但是基本上来说,大多数的make都支持“makefile”和“Makefile”这两种默认文件名。
当然,你可以使用别的文件名来书写Makefile,比如:“Make.Linux”,“Make.Solaris”,“Make.AIX”等,如果要指定特定的Makefile,你可以使用make的 -f 和 --file 参数,如: make -f Make.Linux 或 make --file Make.AIX 。
makefile的引用
在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:
include <filename>filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。
在 include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include 和 <filename> 可以用一个或多个空格隔开。举个例子,你有这样几个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.mkmake命令开始时,会找寻 include 所指出的其它Makefile,并把其内容安置在当前的位置。就好像C/C++的 #include 指令一样。如果文件都没有指定绝对路径或是相对路径的话,make会在当前目录下首先寻找,如果当前目录下没有找到,那么,make还会在下面的几个目录下找:
- 如果make执行时,有
-I或--include-dir参数,那么make就会在这个参数所指定的目录下去寻找。 - 如果目录
<prefix>/include(一般是:/usr/local/bin或/usr/include)存在的话,make也会去找。
makefile的工作流程
GNU的make工作时的执行步骤如下:
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐晦规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
makefile多目标及静态模式
多目标
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,后面的为参数。这个函数是替换字符串的意思,将$@中的’output’替换为空
静态模式
静态模式可以更加容易地定义多目标的规则,可以让我们的规则变得更加的有弹性和灵活。我们还是先来看一下语法:
<targets ...> : <target-pattern> : <prereq-patterns ...>
<commands>
...targets定义了一系列的目标文件,可以有通配符。是目标的一个集合。
target-pattern是指明了targets的模式,也就是的目标集模式。
prereq-patterns是目标的依赖模式,它对target-pattern形成的模式再进行一次依赖目标的定义。
举个例子如果我们的 例子: 上面的例子中,指明了我们的目标从$object中获取, 另一个例子 $(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。其它的内容,我就不用多说了吧。这个例子展示了Makefile中更大的弹性。 当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如: 当我们执行 make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被 make 显示出来 $$ 代表需要使用真实的$字符,这也就不会打印mkdir命令及touch命令%.o ,意思是我们的.o 结尾的,而如果我们的%.c ,意思是对% (也就是去掉了 .o 这个结尾),并为其加上 .c 这个结尾,形成的新集合。
objects = foo.o bar.o
all: $(objects)
$(objects): %.o: %.c
$(CC) -c $(CFLAGS) $< -o $@%.o 表明要所有以 .o 结尾的目标,也就是 foo.o bar.o ,也就是变量 $object 集合的模式,而依赖模式 %.c 则取模式 %.o 的 % ,也就是 foo 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.ofiles = 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 $<命令
exec:
cd /home/hchen
pwd
exec:
cd /home/hchen; pwdmake exec 时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。小知识
$(BUILD_DIR)/.prepared: Makefile
@mkdir -p $$(dirname $@)
@touch $@