How to write a makefile and CmakeLists

How to write a makefile and CmakeLists

2023年8月27日 作者 ScotI_Blog

充分借鉴网络资源

概述 — 跟我一起写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的规则:

  1. 如果这个工程没有编译过,那么我们的所有c文件都要编译并被链接。
  2. 如果这个工程的某几个c文件被修改,那么我们只编译被修改的c文件,并链接目标程序。
  3. 如果这个工程的头文件被改变了,那么我们需要编译引用了这几个头文件的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的工作方式

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

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

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

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

书写规则

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)

另给一个变量使用通配符的例子:

  1. 列出一确定文件夹中的所有 .c 文件。objects := $(wildcard *.c)
  2. 列出(1)中所有文件对应的 .o 文件,在(3)中我们可以看到它是由make自动编译出的:$(patsubst %.c,%.o,$(wildcard *.c))
  3. 由(1)(2)两步,可写出编译并链接所有 .c 和 .o 文件objects := $(patsubst %.c,%.o,$(wildcard *.c)) foo : $(objects) cc -o foo $(objects)

这种用法由关键字“wildcard”,“patsubst”指出,关于Makefile的关键字,我们将在后面讨论。

Print Friendly, PDF & Email