学习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 命令。那么,

  1. make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
  2. 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
  3. 如果edit文件不存在,或是edit所依赖的后面的 .o 文件的文件修改时间要比 edit 这个文件新,那么,他就会执行后面所定义的命令来生成 edit 这个文件。
  4. 如果 edit 所依赖的 .o 文件也不存在,那么make会在当前文件中找目标为 .o 文件的依赖性,如果找到则再根据那一个规则生成 .o 文件。(这有点像一个堆栈的过程)
  5. 当然,你的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里主要包含了五个东西:显式规则、隐晦规则、变量定义、文件指示和注释

  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 键开始

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.Linuxmake --file Make.AIX

makefile的引用

在Makefile使用 include 关键字可以把别的Makefile包含进来,这很像C语言的 #include ,被包含的文件会原模原样的放在当前文件的包含位置。 include 的语法是:

include <filename>

filename 可以是当前操作系统Shell的文件模式(可以包含路径和通配符)。

include 前面可以有一些空字符,但是绝不能是 Tab 键开始。 include<filename> 可以用一个或多个空格隔开。举个例子,你有这样几个Makefile: a.mkb.mkc.mk ,还有一个文件叫 foo.make ,以及一个变量 $(bar) ,其包含了 e.mkf.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. 如果make执行时,有 -I--include-dir 参数,那么make就会在这个参数所指定的目录下去寻找。
  2. 如果目录 <prefix>/include (一般是: /usr/local/bin/usr/include )存在的话,make也会去找。

makefile的工作流程

GNU的make工作时的执行步骤如下:

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

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形成的模式再进行一次依赖目标的定义。

举个例子如果我们的定义成 %.o ,意思是我们的;集合中都是以 .o 结尾的,而如果我们的

定义成 %.c ,意思是对所形成的目标集进行二次定义,其计算方法是,取模式中的 % (也就是去掉了 .o 这个结尾),并为其加上 .c 这个结尾,形成的新集合。

例子:

objects = foo.o bar.o

all: $(objects)

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

上面的例子中,指明了我们的目标从$object中获取, %.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.o

另一个例子

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 $<

$(filter %.o,$(files))表示调用Makefile的filter函数,过滤“$files”集,只要其中模式为“%.o”的内容。其它的内容,我就不用多说了吧。这个例子展示了Makefile中更大的弹性。

命令

当依赖目标新于目标时,也就是当规则的目标需要被更新时,make会一条一条的执行其后的命令。需要注意的是,如果你要让上一条命令的结果应用在下一条命令时,你应该使用分号分隔这两条命令。比如你的第一条命令是cd命令,你希望第二条命令得在cd之后的基础上运行,那么你就不能把这两条命令写在两行上,而应该把这两条命令写在一行上,用分号分隔。如:

  • 示例一:
exec:
    cd /home/hchen
    pwd
  • 示例二:
exec:
    cd /home/hchen; pwd

当我们执行 make exec 时,第一个例子中的cd没有作用,pwd会打印出当前的Makefile目录,而第二个例子中,cd就起作用了,pwd会打印出“/home/hchen”。

小知识

make 会把其要执行的命令行在命令执行前输出到屏幕上。当我们用“@”字符在命令行前,那么,这个命令将不被 make 显示出来

$(BUILD_DIR)/.prepared: Makefile
	@mkdir -p $$(dirname $@)
	@touch $@

$$ 代表需要使用真实的$字符,这也就不会打印mkdir命令及touch命令


学习makefile
https://codebells.github.io/post/makefile.html
作者
Codebells
发布于
2022年10月31日
更新于
2023年3月2日
许可协议