How to write a makefile and CmakeLists
充分借鉴网络资源
概述 — 跟我一起写Makefile 1.0 文档 (seisman.github.io)
First Part: makefile
mainly from 陈皓《跟我一起写makefile》
概述
——
什么是makefile?或许很多Winodws的程序员都不知道这个东西,因为那些Windows的IDE都为你做了这个工作,但我觉得要作一个好的和professional的程序员,makefile还是要懂。这就好像现在有这么多的HTML的编辑器,但如果你想成为一个专业人士,你还是要了解HTML的标识的含义。特别在Unix下的软件编译,你就不能不自己写makefile了,会不会写makefile,从一个侧面说明了一个人是否具备完成大型工程的能力。
因为,makefile关系到了整个工程的编译规则。一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,哪些文件需要先编译,哪些文件需要后编译,哪些文件需要重新编译,甚至于进行更复杂的功能操作,因为makefile就像一个Shell脚本一样,其中也可以执行操作系统的命令。
makefile带来的好处就是——“自动化编译”,一旦写好,只需要一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个命令工具,是一个解释makefile中指令的命令工具,一般来说,大多数的IDE都有这个命令,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make。可见,makefile都成为了一种在工程方面的编译方法。
现在讲述如何写makefile的文章比较少,这是我想写这篇文章的原因。当然,不同产商的make各不相同,也有不同的语法,但其本质都是在“文件依赖性”上做文章,这里,我仅对GNU的make进行讲述,我的环境是RedHat Linux 8.0,make的版本是3.80。必竟,这个make是应用最为广泛的,也是用得最多的。而且其还是最遵循于IEEE 1003.2-1992 标准的(POSIX.2)。
在这篇文档中,将以C/C++的源码作为我们基础,所以必然涉及一些关于C/C++的编译的知识,相关于这方面的内容,还请各位查看相关的编译器的文档。这里所默认的编译器是UNIX下的GCC和CC。
安排make的规则:
- 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
- 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
- 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的c文件,并链接目标程序。
makefile的基本规则
target … : prerequisites …
recipe
…
…
target
可以是一个object file(目标文件),也可以是一个可执行文件,还可以是一个标签(label)。对于标签这种特性,在后续的“伪目标”章节中会有叙述。
prerequisites
生成该target所依赖的文件和/或target。
recipe
该target要执行的命令(任意的shell命令)。
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说:prerequisites中如果有一个以上的文件比target文件要新的话,recipe所定义的命令就会被执行。
这就是makefile的规则,也就是makefile中最核心的内容。
说到底,makefile的东西就是这样一点,好像我的这篇文档也该结束了。呵呵。还不尽然,这是makefile 的主线和核心,但要写好一个makefile还不够,我会在后面一点一点地结合我的工作经验给你慢慢道来。内容还多着呢。:)
tips:
输入test.exe和./test.exe运行效果不同,主要原因是:
- Windows环境下,系统不会自动搜索当前目录来运行程序。运行test.exe时系统不知道从当前目录执行。
- Unix/Linux环境下,使用./程序文件名会告诉系统程序位于当前目录,需要从当前目录执行。
具体来说:
- Windows下以test.exe运行,系统会在PATH环境变量配置的路径搜索test.exe文件。如果当前目录不在PATH变量,就找不到文件。
- 而./test.exe明确告诉系统,程序位于当前运行命令的目录。等价于当前目录。系统可以直接从当前目录执行文件。
- Unix/Linux下,默认就会搜索当前目录,所以直接test.exe即可。但在Windows需要加上路径说明。
所以:
- Windows下需要加上当前目录前缀./ ,告诉系统从当前目录执行文件。
- 或者可以将编译生成的可执行文件拷贝到已配置在PATH中的目录中,然后直接以程序名运行。
采用./形式运行可避免路径找不到的情况,在Windows下也能体现Unix命令的执行语义。而test.exe可能会找不到文件的情况。
Make的工作方式
- make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到“edit”这个文件,并把这个文件作为最终的目标文件。
- 如果edit文件不存在,或是edit所依赖的后面的
.o
文件的文件修改时间要比edit
这个文件新,那么,他就会执行后面所定义的命令来生成edit
这个文件。 - 如果
edit
所依赖的.o
文件也不存在,那么make会在当前文件中找目标为.o
文件的依赖性,如果找到则再根据那一个规则生成.o
文件。(这有点像一个堆栈的过程) - 当然,你的C文件和头文件是存在的啦,于是make会生成
.o
文件,然后再用.o
文件生成make的终极任务,也就是可执行文件edit
了。
在makefile中使用变量(如何简洁地书写)
比如,我们声明一个变量,叫 objects
, OBJECTS
, objs
, OBJS
, obj
或是 OBJ
,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
于是,我们就可以很方便地在我们的makefile中以 $(objects)
的方式来使用这个变量了
让make自动推导
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个 .o
文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个 .o
文件,它就会自动的把 .c
文件加在依赖关系中,如果make找到一个 whatever.o
,那么 whatever.c
就会是 whatever.o
的依赖文件。并且 cc -c whatever.c
也会被推导出来
清空目录的规则
每个Makefile中都应该写一个清空目标文件( .o
)和可执行文件的规则,这不仅便于重编译,也很利于保持文件的清洁。这是一个“修养”(呵呵,还记得我的《编程修养》吗)。一般的风格都是:
clean:
rm edit $(objects)
更为稳健的做法是:
.PHONY : clean
clean :
-rm edit $(objects)
前面说过, .PHONY
表示 clean
是一个“伪目标”。而在 rm
命令前面加了一个小减号的意思就是,也许某些文件出现问题,但不要管,继续做后面的事。当然, clean
的规则不要放在文件的开头,不然,这就会变成make的默认目标,相信谁也不愿意这样。不成文的规矩是——“clean从来都是放在文件的最后”。
总的来讲,
Makefile里主要包含了五个东西:显式规则、隐式规则、变量定义、指令和注释。
- 显式规则。显式规则说明了如何生成一个或多个目标文件。这是由Makefile的书写者明显指出要生成的文件、文件的依赖文件和生成的命令。
- 隐式规则。由于我们的make有自动推导的功能,所以隐式规则可以让我们比较简略地书写Makefile,这是由make所支持的。
- 变量的定义。在Makefile中我们要定义一系列的变量,变量一般都是字符串,这个有点像你C语言中的宏,当Makefile被执行时,其中的变量都会被扩展到相应的引用位置上。
- 指令。其包括了三个部分,一个是在一个Makefile中引用另一个Makefile,就像C语言中的include一样;另一个是指根据某些情况指定Makefile中的有效部分,就像C语言中的预编译#if一样;还有就是定义一个多行的命令。有关这一部分的内容,我会在后续的部分中讲述。
- 注释。Makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用
#
字符,这个就像C/C++中的//
一样。如果你要在你的Makefile中使用#
字符,可以用反斜杠进行转义,如:\#
。
最后,还值得一提的是,在Makefile中的命令,必须要以 Tab
键开始。
GNU的make工作时的执行步骤如下:(想来其它的make也是类似)
- 读入所有的Makefile。
- 读入被include的其它Makefile。
- 初始化文件中的变量。
- 推导隐式规则,并分析所有规则。
- 为所有的目标文件创建依赖关系链。
- 根据依赖关系,决定哪些目标要重新生成。
- 执行生成命令。
书写规则
1.在规则中使用通配符
如果我们想定义一系列比较类似的文件,我们很自然地就想起使用通配符。make支持三个通配符: *
, ?
和 ~
。这是和Unix的B-Shell是相同的。
波浪号( ~
)字符在文件名中也有比较特殊的用途。如果是 ~/test
,这就表示当前用户的 $HOME
目录下的test目录。而 ~hchen/test
则表示用户hchen的宿主目录下的test 目录。(这些都是Unix下的小知识了,make也支持)而在Windows或是 MS-DOS下,用户没有宿主目录,那么波浪号所指的目录则根据环境变量“HOME”而定。
通配符代替了你一系列的文件,如 *.c
表示所有后缀为c的文件。一个需要我们注意的是,如果我们的文件名中有通配符,如: *
,那么可以用转义字符 \
,如 \*
来表示真实的 *
字符,而不是任意长度的字符串。
好吧,还是先来看几个例子吧:
clean:
rm -f *.o
其实在这个clean:后面可以加上你想做的一些事情,如果你想看到在编译完后看看main.c的源代码,你可以在加上cat这个命令,例子如下:
clean:
cat main.c
rm -f *.o
其结果你试一下就知道的。 上面这个例子我不不多说了,这是操作系统Shell所支持的通配符。这是在命令中的通配符。
print: *.c
lpr -p $?
touch print
上面这个例子说明了通配符也可以在我们的规则中,目标print依赖于所有的 .c
文件。其中的 $?
是一个自动化变量,我会在后面给你讲述。objects = *.o
上面这个例子,表示了通配符同样可以用在变量中。并不是说 *.o
会展开,不!objects的值就是 *.o
。Makefile中的变量其实就是C/C++中的宏。如果你要让通配符在变量中展开,也就是让objects的值是所有 .o
的文件名的集合,那么,你可以这样:objects := $(wildcard *.o)
另给一个变量使用通配符的例子:
- 列出一确定文件夹中的所有
.c
文件。objects := $(wildcard *.c) - 列出(1)中所有文件对应的
.o
文件,在(3)中我们可以看到它是由make自动编译出的:$(patsubst %.c,%.o,$(wildcard *.c)) - 由(1)(2)两步,可写出编译并链接所有
.c
和.o
文件objects := $(patsubst %.c,%.o,$(wildcard *.c)) foo : $(objects) cc -o foo $(objects)
这种用法由关键字“wildcard”,“patsubst”指出,关于Makefile的关键字,我们将在后面讨论。