makefile

本系列参考 一起跟我写makefile,在此向原作者致谢。文章经过了我个人的整理、补充、修正、再次排版而完成,记录在此以供大家学习分享。
另附GNU make中文手册以供参考(两者内容高度重合,也许这就是中文社区特色吧,还是推荐看中文手册,表述更清晰准确)。

注:本系列内容大部分是基于GNU make的标准,其中cc命令默认调用Linux自带的c编译器程序

  1. makefile介绍
  2. makefile编写(1):规则
  3. makefile编写(2):变量
  4. makefile编写(3):条件执行和函数

为什么使用makefile

虽然可以直接调用编译器如g++编译程序,但是如果项目里的代码文件变多了,哪些代码文件更新了需要重新编译,哪些代码没有改不需要重新编译等等,靠程序员自己记忆去处理是比较麻烦的事,还有哪些代码需要预处理或是链接哪些库文件,这些都是繁杂的过程。为了规范程序的编译生成过程,产生了规范化的生成脚本,就是makefile,生成器make可以依据规范的makefile自动生成目标程序或库文件。makefile也是一个研究项目的利器,很多项目可能文档不完整,而makefile就是项目的地图,从Makefile入手可以快速窥探整个项目的框架和概貌,深入代码而不至于迷路。
简单的说,就是定义好makefile,可以让程序员只需要去关注如何编写代码,而生成程序过程中的脏活累活都交给make程序。而且现在makefile通常都有工具自动生成,如cmake、qmake工具,这样就大量减轻了程序员的负担。

简单提一下CMake工具:CMake(Cross platform Make)是一个开源的跨平台自动化构建工具,可以跨平台地生成各式各样的makefile或者project文件,支持利用各种编译工具生成可执行程序或链接库。CMake自己不编译程序,它相当于用自己的构建脚本CMakeLists.txt,让各种编译工具集去生成可执行程序或链接库。一般用于编译程序的makefile文件比较复杂,自己去编写比较麻烦,而利用CMake,就可以编写相对简单的CMakeLists.txt,由CMake根据CMakeLists.txt自动生成makefile,然后就可以用make生成可执行程序或链接库。著名的Linux KDE桌面环境的茫茫多程序都是用CMake脚本构建的(用qt开发的),另外跨平台的程序/库如Boost C++ Libraries、OpenCV、LLVM、Clang等也都是用CMake脚本构建的。以后如果接触到这些东西,是需要了解CMake的。
CMake :项目主页
KDE :项目主页

了解makefile并具备手动编写的能力是很重要的。makefile关系到了整个工程的编译规则,一个工程中的源文件不计数,其按类型、功能、模块分别放在若干个目录中,makefile定义了一系列的规则来指定,文件编译的先后顺序,是否需要重新编译,并且在makefile中可以调用Shell脚本与操作系统进行交互,进而执行更加复杂的功能操作。

  • 虽然创建makefile的意义一般是作为生成脚本,但其可拓展性空间非常大。在其中的命令不一定只是使用编译器,完全加入其它的命令,如:tar、awk、mail、sed、cvs、compress、ls、rm、yacc、rpm、ftp等等,来完成诸如“打包”、“备份”、“制作安装包”、“提交代码”、“使用模板”、“合并文件”等等五花八门的功能,涉及文件操作,文件管理,开发设计,或是其它一些异想天开的功能
  • 比如在编写银行交易程序时,由于交易程序基本一样,于是有人编写了一些通用程序模板,在该模板中把一些网络通讯、数据库操作、业务操作共性的东西写在一个文件中,在这些文件中用些诸如"@@@N""###N"奇怪字串标注一些位置,然后编写交易业务时只需按照一种特定规则处理,最后在make时使用awk和sed把模板中的"@@@N""###N"等字串替代成特定的程序形成c语言文件。

一个简单的实例

简单程序可以自己一句句敲g++命令,如果项目复杂起来,代码太多了,自己敲命令编译就很麻烦,而且一个.cpp文件修改后就得重新生成目标文件*.o,因此实际开发项目时都是借助make工具(MinGW的是mingw32-make),编写好makefile之后,只需要在项目文件夹执行一句make命令,其他生成目标文件、链接目标文件和库以及自动根据源代码改动重新生成等,这些事情全交给make,而程序员就不用操心构建程序的具体过程。
makefile 文件可以自己编写,其实绝大多数的集成开发环境都可以根据项目文件自动生成相应的makefile,所以实际中很多都是集成开发环境自动完成的。
这里示范一个简单的makefile,体会一下生成器make是如何工作的。
在上面hellorect文件夹里新建一个文件,命名为makefile,不要带扩展名。使用Notepad2工具或记事本打开该makefile文件,里面输入如下脚本:

# makefile for building: hellorect
CC = gcc
CXX = g++
LINKER = g++
LFLAGS = -lm -static

OBJECTS = rect.o hellorect.o
DSTTARGET = hellorect
# Default rule
all: $(DSTTARGET)

$(DSTTARGET): $(OBJECTS)
	$(LINKER)  $(LFLAGS)  -o $@  $(OBJECTS)

hellorect.o: hellorect.cpp
	$(CXX) -c  -o  $@  $<  

rect.o: rect.cpp
	$(CXX) -c  -o  $@  $<  

clean:
	rm  $(OBJECTS)  hellorect

这里解释一下上面脚本的意思

  • # 打头的是注释,忽略掉
  • 中间带有等于号的都是定义变量,引用变量的方式就是

$(变量名)

  • CC是c语言编译器,CXX是c++编译器,LINKER是链接器,LFLAGS是链接器的参数。OBJECT是编译得到的目标文件,DSTTARGET是链接后的可执行的目标程序。
    • makefile只是一个生成脚本,因此其解释执行的过程并不涉及复杂的符号存储链接的问题,makefile的变量基本上很类似于将宏展开为字符串的过程

makefile基本生成规则

接下来是makefile的生成规则,makefile的基本规则是:

生成目标: 依赖文件
[tab字符] 系统命令

上例的makefile中

all: $(DSTTARGET)

是默认生成规则,依赖文件$(DSTTARGET),它的下一行没有命令。
而如何生成$(DSTTARGET)呢,继续往下找

$(DSTTARGET): $(OBJECTS)

生成 $(DSTTARGET) 需要 $(OBJECTS),有了目标文件之后执行命令

$(LINKER) $(LFLAGS) -o $@ $(OBJECTS)

即调用链接器$(LINKER),根据链接器参数$(LFLAGS)和$(OBJECTS),生成$@。$@就是上一行冒号左边的要生成的目标。注意系统命令$(LINKER)之前一定要有制表符tab字符,不能用4个空格代替,否则make时会出现没有分隔符(separator)的错误。

接下来的四句:

hellorect.o: hellorect.cpp  
	$(CXX) -c  -o  $@  $<  

rect.o: rect.cpp
	$(CXX) -c  -o  $@  $< 

是使用编译器生成目标文件hellorect.o和rect.o,$@是上一行冒号左边的目标,$<是上一行冒号右边第一个依赖文件。hellorect.o和rect.o就是链接器需要的$(OBJECTS) 。

最后的两句是清除规则:

clean:
	rm  $(OBJECTS)  hellorect

rm是删除命令,如果Windows系统里没有rm命令,请安装一个msysgit工具(下载地址), 然后系统环境变量里面会有msysgit工具路径,里面有rm工具。
这里clean做的事情就是删除项目生成的.o和可执行文件。(注:Windows系统里可执行程序有.exe后缀,需要加上.exe后缀。)每个makefile中都应该写一个清空目标文件(.o和执行文件)的规则,这不仅便于重编译,也很利于保持文件的清洁。clean的规则不要放在文件的开头,不然这就会变成make的默认目标,相信谁也不愿意这样,不成文的规矩是——“clean从来都是放在文件的最后”。

make自动管理编译文件

编辑好makefile文件之后,那么如何使用make工具呢?如果要生成项目,就在项目文件夹hellorect里执行:

make

如果要清理项目就执行:

make clean

Linux 系统里直接用make,MinGW里面是用mingw32-make生成程序,Windows系统里用mingw32-make。

在默认的方式下,也就是我们只输入make命令。那么,

  • make会在当前目录下找名字叫“Makefile”或“makefile”的文件
  • 如果找到,它会找文件中的第一个目标文件(target),在上面的例子中,他会找到hellorec这个文件,并把这个文件作为最终的目标文件
    • 如果edit文件不存在,或是edit所依赖的后面的.o文件的文件修改时间要比edit这个文件新,那么,他就会执行后面所定义的命令来生成DSTTARGET这个文件
  • 如果edit所依赖的.o文件也不存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件
    • 这有点像一个堆栈的过程,把实际编译的顺序倒过来压栈,当到已经存在的依赖文件后再依次编译生成栈中的目标
  • 当然,一般源文件和头文件肯定是存在,于是make会生成.o文件,然后再用.o文件完成make的终极任务的编译,也就是执行文件DSTTARGET了
  • 考虑到头文件的用途,一般源文件也是要依赖于头文件的,因此头文件改掉的话基本上整个工程都要重新编译了

这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系(而且是每完成一步编译任务应该就生成下一个任务的输入),直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make根本不理。make只管文件的依赖性,即如果找了依赖关系之后,冒号后面的文件还是不在,那么对不起,make就不工作啦。
通过上述分析,可以知道像clean这种,没有被第一个目标文件直接或间接关联,那么它后面所定义的命令将不会被自动执行,不过可以显式让make执行,即命令make clean,以此来清除所有的目标文件,以便重编译。因此在实际中,如果这个工程已被编译过了,当我们修改了其中一个源文件,比如hellorecpp.c,那么根据依赖性hellorec.o会被重编译(也就是在这个依赖关系后面所定义的命令),这时hellorec.o的文件也是最新的啦。hellorec.o的文件修改时间要比edit要新,所以hellorec也会被重新链接。

make的基本工作原理(以GNU make为例)

makefile本质上只是一个文本文件,其中定义的规则是否可以实现要由具体平台的make工具而定(就跟c语言与c编译器的关系一样)。makefile一旦写好,只需要Shell中的一个make命令,整个工程完全自动编译,极大的提高了软件开发的效率。make是一个解释makefile指令的命令行程序,大多数集成开发环境都支持了这一命令工具,比如:Delphi的make,Visual C++的nmake,Linux下GNU的make,不同厂商的make工具之间略有差异,不过这其中使用最多、应用最为广泛的还是GNU的make,相对来说也最遵循于IEEE 1003.2-1992标准(POSIX.2),因此一般不作特殊说明的make都是指GNU的make。
实际上针对这个问题进一步来说,make本身是考虑到跨平台的问题的,但是这里面存在着很重要的一点是make要调用Shell来解释命令,而不同平台不同版本的Shell的区别是非常大的。另外不同平台在文件系统、设备系统等方面的设计都有所不同(最明显的比方说UNIX和Windows的文件路径格式就不一样),所以一般的软件其实都很难做到完全的跨平台。

makefile文件名

注意:makefile文件通常没有扩展名。

  • 主要是linux原本就没有后缀名一说,xxx.xxx就是一个文件名,文件名和linux文件类型没有关系
  • 有时需要编写一组复杂的包含彼此的makefile时,一般会定义一系列的makefile文件,但这些通常也是保留给主文件包含的makefile片段

默认的情况下,make工具会在当前目录下按顺序找寻文件名为GNUmakefile、makefile、Makefile的文件。在这三个文件名中,最好使用makefile这个文件名,因为总感觉这个比较符合变量命名规则,并且重要的是一些其他的make工具只对全小写的makefile文件名敏感。基本上来说,大多数的make工具都支持makefile和Makefile这两种默认文件名,因此最好不要用GNUmakefile,这个文件是GNU的make识别的。

  • 需要注意的是如果你同时定义了GNUmakefile、makefile、Makefile这三个文件名,那么make搜索的优先级为GNUmakefile>makefile>Makefile

当然也可以使用别的文件名来命名makefile,比如:Make.Linux,Make.Solaris,Make.AIX等。要指定这些自定义命名的makefile并且作为make的生成脚本,可以使用make的-f和–file(或者有的版本–makefile)参数,如果在make命令中不只一次地使用了-f参数,那么所有指定的makefile将会被连在一起传递给make执行,如:

make -f Make.Linux --file Make.AIX

makefile的构成

绝大多数的makefile里主要包含了五个东西:显式规则、隐式规则、变量定义、文件指示和注释。

  • 显式规则,显式规则说明了,如何生成一个或多的的目标文件,这是由makefile的编写者显式指定的,如要生成的文件,生成文件的依赖文件,生成的shell命令等。makefile中的命令必须要以[Tab]键开始
    • 实际上makefile由两种语言的语法构成,一种是makefile原生语法,另一个是Shell脚本语法,这两者间就看开始是否使用[Tab]键缩进来区分
  • 隐晦规则,主要是由make工具具体支持的自动推导的功能,方便脚本编写
  • 变量的定义,在makefile中可以定义一系列的变量,变量一般都是字符串
  • 文件指示,其中主要包括三个部分
    • 在一个makefile中引用另一个makefile,就像用include包含头文件一样
    • 根据某些情况指定makefile中的有效部分,就像预编译指令#if一样
    • 定义一个多行的命令
  • 注释,makefile中只有行注释,和UNIX的Shell脚本一样,其注释是用#字符。如果要在makefile中使用#字符,可以用反斜框进行转义,如:\#

makefile文件包含

在makefile中使用include关键字可以把别的makefile包含进来,这很像c语言的#include,被包含的文件会完全一致地扩展在当前文件的包含位置。
include的语法是:

include<filename>

其中filename可以是当前操作系统Shell的文件模式(也就是说可以包含通配符)。在include前面可以有一些空字符,但是绝不能以[Tab]键开始。include和filename间可以用一个或多个空格隔开,假设在目录中有几个以.mk结尾的makefile,那么可以这样包含:

include foo.make *.mk

或者也可以用文件名定义变量,假设用上面这些以.mk结尾的makefile定义变量$(bar),也可以这样包含:

include foo.make $(bar)

make命令开始时,会搜索include所指出的其它makefile,并把其内容安置在当前的位置。如果文件都没有指定绝对路径的话,make会在当前目录下首先搜索,如果当前目录下没有找到那么make还会在下面的几个目录下找:

  • 如果make执行时,有"-I"或"--include-dir"参数,那么make就会在这个参数所指定的目录下去找
  • 如果目录/include(一般是:/usr/local/bin/include或/usr/include)存在的话,make也会去这里找
  • 如果当前环境中定义了环境变量MAKEFILES,那么make会把这个变量中的值做一个类似于include的操作。这个变量中的值是其它的makefile路径,用空格分隔。只是它和include不同的是从这个环境变量中引入的makefile的目标文件不会起作用,如果环境变量中定义的文件发现错误,make也会不理
    • makefile会自动读取系统中的环境变量,并复制一份一模一样的,如果用户在makefile中定义的同名的变量,那么原来的环境变量就会被覆盖
    • 最好慎重使用MAKEFILES这个环境变量,因为只要这个默认保留的变量一被定义,那么使用make时所有makefile都会受其影响。(也就是说所有进程相关的makefile都自动包含MAKEFILES中的文件,显然这是个很有问题的做法)

如果有文件没有找到或无法读的话,make会生成一条警告信息,但不会马上出现错误。它会继续载入其它的文件,一旦完成整个makefile的读取,make会再重试这些没有找到或不能读取的文件,如果还是不行,make才会出现一条错误信息。如果想让make忽略那些无法读取的文件继续执行,可以在include前加一个减号,其表示无论include过程中出现什么错误都不要报错继续执行,如:

-include<filename>

还有一个相关命令是sinclude,其作用和include是一样的,sinclude的意义在于和其它版本的make工具兼容。

make工具的主要工作流程

  1. 读入环境变量MAKEFILES定义的makefile文件列表
  2. 读入当前目录下的makefile文件
    • 根据命名查找顺序,按顺序查找GNUmakefile、makefile、Makefile,找到哪个就读取哪个
  3. 读入被include的其它makefile
  4. 查找并重建所有已读取的makefile文件的规则
    • 如果存在一个目标是当前读取的某一个makefile文件,则执行此规则重建此makefile文件,完成以后从第一步开始重新执行(也就是说一个makefile可能牵扯到另一个makefile的内容,并且这个makefile还包含了对方,因此要在重建包含的makefile之后重新执行这个makefile,总之这种情况很复杂且难以控制,最好避免)
  5. 初始化文件中的变量,推导隐式规则,并分析所有规则,展开那些需要立即展开的变量和函数并根据预设条件确定执行分支
  6. 根据最终目标以及其他目标的依赖关系建立依赖关系链表
  7. 执行除最终目标以外的所有的目标的规则
    • 规则中如果依赖文件中任一个文件的时间戳比目标文件新,则使用规则所定义的命令重建目标文件
  8. 执行最终目标所在的规则

1-6步为读取和分析阶段,7-8为执行阶段。第一个阶段中,如果定义的变量被使用了,那么make会把其展开在使用的位置。但此时make一般并不会完全马上展开,如果变量出现在依赖关系的规则中,那么仅当这条依赖被决定要使用了,变量才会在其内部展开(makefile毕竟是脚本按行执行,不需要编译,所以运行时再展开很正常)。

makefile的执行

一般来说,执行makefile最简单的方法就是直接在命令行下输入make命令,make命令会找当前目录下的makefile来执行,自动编译过时目标文件。但也有时或许只想让make重编译某些文件,而不是整个工程,而又有时候可能有几套编译规则,想要在不同的时候使用不同的编译规则,这时可以采用一些命令参数选项执行make命令以实现一些特殊目的。

make的退出码

make命令执行后有三个退出码

  • 0 —— 表示成功执行,无异常地返回
  • 1 —— 如果make运行时出现任何错误,其返回1。
  • 2 —— 如果使用了make的-q选项,并且make使得一些目标不需要更新,那么返回2

make的命令行参数

下面列举了所有GNU make 3.80版的参数定义。其它版本和产商的make大同小异,不过其它产商的make的具体参数还是请参考各自的产品文档。

  • -b -m:这两个参数的作用是忽略和其它版本make的兼容性。
  • -B --always-make:认为所有的目标都需要更新(重编译)。(不根据规则的依赖描述决定)
  • -C <dir> --directory=<dir>:指定读取makefile的目录。如果有多个-C参数,make的解释是后面的路径以前面的作为相对路径,并以最后的目录作为被指定目录
    • 如:make -C ~hchen/test -C prog 等价于 make -C ~hchen/test/prog
  • –debug[=<options>]:输出make的调试信息,有几种不同的级别可供选择,如果没有参数默认输出最简单的调试信息(-b),下面是<options>的取值:
    • a(all):输出所有的调试信息。(会有非常的多的信息)
    • b(basic):只输出简单的调试信息。即输出不需要重编译的目标
    • v(verbose):在b选项的级别之上,输出的信息包括哪个makefile被解析,不需要被重编译的依赖文件(或是依赖目标)等
    • i(implicit):输出所有用到的隐含规则
    • j(jobs):输出执行规则中子进程的详细信息,如命令的PID、返回码等
    • m(makefile):输出make读取、更新、执行makefile的信息
  • d:相当于–debug=a
  • -e --environment-overrides:指明环境变量的值覆盖makefile中定义的变量的值
  • -f=<file> --file=<file> --makefile=<file>:指定需要执行的makefile
  • -h --help:显示帮助信息
  • -i --ignore-errors:在执行时忽略所有的错误
  • -I <dir> --include-dir=<dir>:指定一个被包含makefile的搜索目标,可以使用多个-I参数来指定多个目录
  • -j [<jobsnum>] --jobs[=<jobsnum>]:指并发运行命令的个数,如果没有这个参数make则只能单任务运行,如果有一个以上的-j参数,那么仅最后一个-j才是有效的。
  • -k --keep-going:即时出错也不停止运行,不过如果生成一个目标失败了那么依赖于其上的目标也是不会被执行的(类似于把error换成warning)
  • -l <load> --load-average[=<load>] --max-load[=<load>]:指定make运行命令的负载
  • -n --just-print --dry-run --recon:仅输出执行过程中的命令序列,但并不执行
  • -o <file> --old-file=<file> --assume-old=<file>:不重新生成指定的<file>,即使这个目标的依赖文件更新
  • -p --print-data-base:输出makefile中的所有数据,包括所有的规则和变量,这个参数会让一个简单的makefile都会输出一堆信息
    • 如果只是想输出信息而不想执行,可以使用make -qp
    • 如果想查看执行makefile前的预设变量和规则,可以使用make –p –f /dev/null,输出的信息包括makefile的文件名和行号(但是我的测试结果并没有),所以用来调试makefile会很有用,特别是当环境变量很复杂的时候
  • -q --question:不运行命令也不输出,仅仅是检查是否有目标需要更新,如果是0则说明要更新,否则返回1(如果是2则说明有错误发生)
  • -r --no-builtin-rules:禁止make使用任何隐含规则
  • -R --no-builtin-variabes:禁止make使用任何作用于变量上的隐含规则
  • -s --silent --quiet:在命令运行时不输出命令的输出
  • -S --no-keep-going --stop:取消-k选项的作用
    • 因为有些时候,make的选项是从环境变量MAKEFLAGS中继承下来的而并不是本身想要的,所以可以在命令行中使用这个参数来让环境变量中的-k选项失效
  • -t --touch:相当于UNIX的touch命令,只是把目标的修改日期变成最新的,同时也能阻止生成目标的命令运行
  • -v --version:输出make程序的版本、版权等关于make的信息
  • -w --print-directory:输出运行makefile之前和之后的信息,这个参数对于跟踪嵌套式调用make的情况很有用
  • –no-print-directory:禁止-w选项
  • -W <file> --what-if=<file> --new-file=<file> --assume-file=<file>:假定目标<file>已经更新
    • 如果和-n选项使用,那么这个参数会输出该目标更新时的运行动作
    • 如果没有-n那么相当于-t选项
  • –warn-undefined-variables:只要make发现有未定义的变量,那么就输出警告信息

下面是我自己系统版本的make的help信息,内容差不多(其中多了一个-E,看起来是用于从命令行输入eval语句?):

make命令行参数的的典型用法

替代命令执行

多数情况下使用makefile的目的就是为了告诉make一个目标是否过期,从而重建一个过期的目标。但是在某些时候可能并不希望真正更新那些已经过期的目标文件,比如只是检查更新目标的命令是否正确,或者察看哪些目标需要更新。要实现这样的目的可以使用一些特定的参数来限定make执行的动作,通过指定参数替代make默认动作执行方式的行为称作替代命令执行,这些行为包括:

  • -n --just-print --dry-run --recon
    • 指定make执行空操作(不执行规则的命令),只打印出需要重建的目标使用的命令(过期目标的重建命令),而不对目标进行重建
  • -q --question
    • 让make返回给定(没有指定则是终极目标)的目标是否是最新的,可以根据它的返回值来判断是否需要真正的执行更新目标的动作
  • -t --touch
    • 更新所有目标文件的时间戳,使得过时的目标文件可以不进行内容更新
  • -W FILE --what-if= FILE --assume-new= FILE --new-file= FILE
    • 通常是指定一个存在的源文件,make假设这个文件被修改过,但不真正的更改文件本身的时间戳,因此这个文件的时间戳被认为最新的,在执行时依赖于这个文件的目标将会被重建

-W通常可以和其他几个命令结合起来使用,比如-W和-n参数一同使用的话就可以查看假设-W文件更改后哪些文件需要被更新,但不用真的去更新。-W和-q一同使用的话那么如果没有错误发生的话则一般返回1(因为-W参数会使所有依赖链文件都需要更新,除非指定的这个文件没有依赖文件)。如果将-W和-t混用的话,此时只对依赖于-W指定文件的更新时间戳,需要说明的是,make在对文件执行touch时并不需要Shell,它自己本身可以直接操作。

防止特定文件重建

有时当修改了工程中的某一个文件后,并不希望重建那些依赖于这个文件的目标。比如在给一个头文件中加入了一个宏定义、或者一个增加的函数声明,这些修改不会对已经编译完成的程序产生任何影响,但在执行make时因为头文件的改变会导致所有包含它的源文件被重新编译(除非模块是独立的,或者makefile的规则链的定义本身就存在缺陷),这种情况为了避免重新编译整个工程,就要进行一些特殊的处理:

  • 首先可以对未更改头文件(当然只是作为例子,有些情况不一定是头文件)之前的编译程序进行保存(类似于备份),这样当make更新了整个程序后,可以将除了要修改的头文件以外的部分“回滚”到修改头文件之前的状态
    • 这种方法在实际项目中并不太实际,更何况修改是任意时刻都可能发生的,而其中的有些修改是需要重建的,很少有项目支持这样复杂的日志管理和恢复功能
  • 编辑包含头文件的那些源文件,将这些文件的时间戳拉到修改的文件之后
    • 问题在于在不影响实现功能的前提下修改有时候并不容易做到
  • 使用-o选项指定修改的头文件,这样该头文件的修改就不会触发依赖它的目标被重建
    • 注意这种方法仅对头文件适用,对源文件的修改可能不奏效
  • 最为直接的方法:使用-t选项直接把所有文件时间戳都拉到最新(可能存在副作用,比如有其他需要重建的地方没法重建了)

替换变量定义

执行make时,一个含有=的命令行参数V=X的含义是定义变量V的值为X,并将这个变量作为make的参数,这种方式定义的变量会替代makefile中的同名变量定义(如果存在并且在makefile中没有使用指示符override对这个变量进行声明),这个过程被称之命令行参数定义覆盖普通变量定义。最典型的例子是CFLAGS,可以在make命令中指定CFLAGS的值为所有makefile都需要的编译选项,然后在各个makefile文件中根据各自需要通过override和追加赋值的方式增加它们自己的编译选项。通过命令行定义变量跟在makefile中定义变量一样也存在两种风格的变量定义:递归展式定义和直接展开式定义,不过除非在命令行定义的变量值中包含了对其他变量或者函数的引用,否则这两种方式在此是等价的(大概是在命令行中的变量是在命令行中判断展开的,和makefile中的变量展开是独立的过程,并且发生在读入makefile之前)。

使用make进行编译测试

正常情况make在执行makefile时,如果出现命令执行的错误,会立即放弃继续执行并返回一个非0的退出码,错误发生点之后的命令将不会被执行。一个错误的发生就表明了最终目标必定将不能被重建,但是假如在修改了一些源文件之后重新编译工程,此时希望的是在某一个文件编译出错以后能够继续进行后续文件的编译,直到最后出现链接错误时才退出,这样可以了解所修改的文件中哪些文件没有修改正确,在下一次编译之前能对出现错误的所有文件一并进行改正,而不是编译一次只能定位一个错误。为了实现这个目的,可以使用-k或–keep-going选项,此时当出现错误时继续执行(给出一个错误信息),直到最后无法生成或重建最终目标才返回非0并退出。