makefile 中文手册 第五章规则的命令.docx
《makefile 中文手册 第五章规则的命令.docx》由会员分享,可在线阅读,更多相关《makefile 中文手册 第五章规则的命令.docx(14页珍藏版)》请在冰豆网上搜索。
![makefile 中文手册 第五章规则的命令.docx](https://file1.bdocx.com/fileroot1/2022-12/7/273240cc-8a01-4a07-95d2-868feeecf4a1/273240cc-8a01-4a07-95d2-868feeecf4a11.gif)
makefile中文手册第五章规则的命令
第五章:
规则的命令
规则的命令由一些shell命令行组成,它们被一条一条的执行。
规则中除了第一条紧跟在依赖列表之后使用分号隔开的命令以外,其它的每一行命令行必须以[Tab]字符开始。
多个命令行之间可以有空行和注释行(所谓空行,就是不包含任何字符的一行。
如果以[Tab]键开始而其后没有命令的行,此行不是空行。
是空命令行,在执行规则时空行被忽略。
通常系统中可能存在多个不同的shell。
但在make处理Makefile过程时,如果没有明确指定,那么对所有规则中命令行的解析使用“/bin/sh”来完成。
执行过程所使用的shell决定了规则中的命令的语法和处理机制。
当使用默认的“/bin/sh”时,命令中出现的字符“#”到行末的内容被认为是注释。
当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注视处理。
另外在make解析makefile文件时,对待注释也是采用同样的处理方式。
我们的shell脚本也一样。
5.1命令回显
通常,make在执行命令行之前会把要执行的命令行输出到标准输出设备。
我们称之为“回显”,就好像我们在shell环境下输入命令执行时一样。
但是,如果规则的命令行以字符“@”开始,则make在执行这个命令时就不会回显这个将要被执行的命令。
典型的用法是在使用“echo”命令输出一些信息时。
如:
@echo开始编译XXX模块......
执行时,将会得到“开始编译XXX模块......”这条输出信息。
如果在命令行之前没有字符“@”,那
么,make的输出将是:
echo编译XXX模块......
编译XXX模块......
另外,如果使用make的命令行参数“-n”或“--just-print”,那么make执行时只显示所要执行的命令,但不会真正的去执行这些命令。
只有在这种情况下make才会打印出所有make需要执行的命令,其中也包括了使用“@”字符开始的命令。
这个选项对于我们调试Makefile非常有用,使用这个选项我们可以按执行顺序打印出Makefile中所有需要执行的所有命令。
而make参数“-s”或“--slient”则是禁止所有执行命令的显示,就好像所有的命令行均使用“@”开始一样。
在Makefile中使用没有依赖的特殊目标“.SILENT”也可以禁止命令的回显,但是它不如使用“@”来的灵活。
因此在书写Makefile时,我们推荐使用“@”来控制命令的回显。
5.2命令的执行
规则中,当目标需要被重建时。
此规则所定义的命令将会被执行,如果是多行命令,那么每一行命令将在一个独立的子shell进程中被执行(就是说,每一行命令的执行是在一个独立的shell进城中完成。
因此,多行命令之间的执行是相互独立的,相互之间不存在依赖(多条命令行的执行为多个相互独立的进程。
在Makefile中书写在同一行中的多个命令属于一个完整的shell命令行,书写在独立行的一条命令是一个独立的shell命令行。
因此:
在一个规则的命令中,命令行“cd”改变目录不会对其后的命令的执行产生影响。
就
是说其后的命令执行的工作目录不会是之前使用“cd”进入的那个目录。
如果要实现这个目的,就不能
把“cd”和其后的命令放在两行来书写。
而应该把这两条命令写在一行上,用分号分隔。
这样它们才是一个完整的shell命令行。
如:
foo:
bar/lose
cdbar;gobblelose>../foo
如果希望把一个完整的shell命令行书写在多行上,需要使用反斜杠(\来对处于多行的命令进行连接,表示他们是一个完整的shell命令行。
例如上例我们以也可以这样书写:
foo:
bar/lose
cdbar;\
gobblelose>../foo
make对所有规则命令的解析使用环境变量“SHELL”所指定的那个程序,在GNUmake中,默认的程序是“/bin/sh”。
不像其他绝大多数变量,它们的值可以直接从同名的系统环境变量那里获得。
make的环境变
量“SHELL”没有使用系统环境变量的定义。
因为系统环境变量“SHELL”指定那个程序被用来作为用户和系统交互的接口程序,它对于不存在直接交互过程的make显然不合适。
在make的环境变量中“SHELL”会被重新赋值;它作为一个变量我们也可以在Makefile中明确地给它赋值(指出解释程序的名字,当明确指定时需要使用完整的路径名。
如“/bin/sh”,变量“SHELL”的默认值是“/bin/sh”。
(在MS-DOS下有些不同,MS-DOS不存在SHELL环境变量。
这里不对MS-DOS下make进行介绍,有兴趣地可以自行参考infomake关于此部分的描述
5.3并发执行命令
GNUmake支持同时执行多条命令。
通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令执行完成之后才能够开始执行。
不过可以通过make的命令行选项“-j”或者“--job”来告诉make在同一时刻可以允许多条命令同时被执行(注意,在MS-DOS中此选项无效,因为它是单任务操作系统。
如果选项“-j”之后存在一个整数,其含义是告诉make在同一时刻可允许同时执行命令的数目。
这个数字被称为“jobslots”。
当“-j”选项之后没有出现一个数字时,那么同一时刻执行的命令数目没有要求。
使用默认的“jobslots”,值为1。
表示make将串行的执行规则的命令(同一时刻只能有一条命令被执行。
并行执行命令所带来的问题是显而易见地:
1.多个同时执的命令的输出信息将同时被输出到终端。
当出现错误时很难根据一大堆凌乱的信息来区分是哪条命令执行错误。
2.在同一时刻可能会存在多个命令执行进程同时读取标准输入,但是对于标准输入设备来说,在同一时刻只能存在一个进程访问它。
就是说在某个时间点,make只能保证此刻正在执行的进程中的一个进程读取标准输入流,而其它进程的标准输入流将置无效。
因此在一时刻多个执行命令的进程中只能有一个进程获得标准输入,而其它需要读取标准输入流的进程由于输入流无效而导致致命错误(通常此进程会得到操作系统的管道破裂信号而被终止。
这是因为:
执行中的命令在什么时候会读取标准输入流(终端输入或重定向的标准输入是不可预测的。
而得到标准输入的顺序总是按照先来先获得的原则。
那个命令首先被执行,那么它就可以首先得
到标准输入设备。
而其它后续需要获取标准输入设备的命令执行进程,由于不能得到标准输入而产生致命错误。
在Makefile规则中如果存在很多命令需要读取标准输入设备,而它们又被允许并行执行时,就会出现这样的错误。
为了解决这个问题。
我们可以修改Makefile规则的命令使之在执行过程中避免使用标准输入。
当然也可以只存在一个命令在执行时会访问标准输入流的Makefile。
3.会导致make的递归调用出现问题。
当make在执行命令时,如果某一条命令执行失败(被一个信号中止,或非零退出,且该条命令产生的错误不可忽略,那么其它的用于重建同一目标的命令执行也将会被终止。
此种情况下,如果make没有使用“-k”或“--keep-going”选项,make将停止执行而退出。
另外:
如果make在执行时,由某种原因(包括信号被中止,此时它的子进程(那些执行规则命令行的shell子进程正在运行,那么make将等到所有这些子进程结束之后才真正退出。
执行make时,如果系统运行于重负荷状态下,我们需要控制(减轻系统在执行make时的负荷。
可以使用“-l”选项告诉make限制当前运行的任务的数量(make所限制的只是它本身所需要占用的系统负载,而不能通过它去控制其它的任务所占用的系统负载。
“-l”或“--max-load”选项一般后边需要跟一个浮点数。
如:
-l2.5
它的意思是告诉make当系统平均负荷高于2.5时,不再启动任何执行命令的子任务。
不带浮点数的“-l”选项用于取消前面通“-l”给定的负荷限制。
更为准确一点就是:
每一次,make在启动一项任务之前(当前系统至少存在make的子任务正在运行。
首先make会检查当前系统的负荷;如果当前系统的负荷高于通过“-l”选项指定的值,那么make就不会在其他任务完成之前启动任何任务。
缺省情况下没有负荷限制。
5.4命令执行的错误
通常;规则中的命令在运行结束后,make会检测命令执行的返回状态,如果返回成功,那么就启动另外一个子shell来执行下一条命令。
规则中的所有命令执行完成之后,这个规则就执行完成了。
如果一个规则中的某一个命令出错(返回非0状态,make就会放弃对当前规则后续命令的执行,也有可能会终止所有规则的执行。
一些情况下,规则中一个命令的执行失败并不代表规则执行的错误。
例如我们使用“mkdir”命令来确保存在一个目录。
当此目录不存在使我们就建立这个目录,当目录存在时那么“mkdir”就会执行失败。
其实我们并不希望mkdir在执行失败后终止规则的执行。
为了忽略一些无关命令执行失败的情况,我们可以在命令之前加一个减号“-”(在[Tab]字符之后,来告诉make忽略此命令的执行失败。
命令中的“-”号会在shell解析并执行此命令之前被去掉,shell所解释的只是纯粹的命令,“-”字符是由make来处理的。
例如对
于“clean”目标我们就可以这么写:
clean:
-rm*.o
其含义是:
即使执行“rm”删除文件失败,make也继续执行。
在执行make时,如果使用命令行选项“-i”或者“—ignore-errors”,make将忽略所有规则中命令执行
的错误。
没有依赖的特殊目标“.IGNORE”在Makefile中有同样的效果。
但是“.IGNORE”的方式已经很少使用,因为它没有在命令行之前使用“-”的方式灵活。
当使用make的“-i”选项或者使用“-”字符来忽略命令执行的错误时,make始终把命令的执行结果作为成功来对待。
但会提示错误信息,同时提示这个错误被忽略。
当不使用这种方式来通知make忽略命令执行的错误时,那么在错误发生时,就意味着定义这个命令规则的目标不能被正确重建,同样,和此目标相关的其它目标也不会被正确重建。
由于先决条件不能建立,那么后续的命令将不会被执行。
在发生这样情况时,通常make会立刻退出并返回一个非0状态,表示执行失败。
像对待命令执行的错误一样,我们可以使用make的命令行选项“-k”或者“--keep-going”来通知make,在出现错误时不立即退出,而是继续后续命令的执行。
直到无法继续执行命令时才异常退出。
例如:
使用“-k”参数,在重建一个.o文件目标时出现错误,make不会立即退出。
虽然make已经知道因为这个错误而无法完成终极目标的重建,但还是继续完成其它后续的依赖文件的重建。
直到执行最后链接时才错误退出。
一般make的“-k”参数在实际应用中,主要用途是:
当同时修改了工程中的多个文件后,“-k”参数可以帮助我们确认对那些文件的修改是正确的(可以被编译,那些文件的修改是不正确的(不能正确编译。
例如我们修改了工程中的20个源文件,修改完成之后使用带“-k”参数的make,它可以一次性找出修改的20个文件中哪些是不能被编译。
通常情况下,执行失败的命令一旦改变了它所在规则的目标文件,则这个改变了的目标可能就不是一个被正确重建的文件。
但是这个文件的时间戳已经被更新过了(这种情况也会发生在使用一个信号来强制中止命令执行的时候。
因此下一次执行make时,由于时间戳更新它将不会被重建,将最终导致终极目标不能被正确重建。
为了避免这种错误的出现,应该在一次make执行失败之后使用“makeclean”来清除已经重建的所有目标,之后再执行make。
我们也可以让make自动完成这个动作,我们只需要在Makefile中定义一个特殊的目标“.DELETE_ON_ERROR”。
但是这个做法存在不兼容。
推荐的做法是:
在make执行失败时,修改错误之后执行make之前,使用“makeclean”明确的删除第一次错误重建的所有目标。
本节的最后,需要说明的是:
虽然make提供了命令行选项来忽略命令执行的错误。
建议对于此选项谨慎使用。
因为在一个大型的工程中,可能需要对成千个源文件进行编译。
编译过程中的任何一个文件编译的错误都是不能被忽略的没,否则可能最后完成的终极目标是一个让你感到非常迷惑的东西,它在运行时可能会产生一些莫名奇妙的现象。
这需要我们保证书写的Makefile中规则的命令在执行时不会发生错误。
特别需要注意哪些有特殊目的的规则中的命令。
当所有命令都可以被正确执行时,我们就没有必要为了避免一些讨厌的错误而使用“-i”选项,为了实现同样的目的,我们可以使用其它的一些方式。
例如删除命令可以这样写:
“$(RM”或者“rm-f”,创建目录的命令可以这样写:
“mkdir–p”等等。
5.5中断make的执行
make在执行命令时如果收到一个致命信号(终止make,那么make将会删除此过程中已经重建的那些规则的目标文件。
其依据是此目标文件的当前时间戳和make开始执行时此文件的时间戳是否相同。
删除这个目标文件的目的是为了确保下一次make时目标文件能够被正确重建。
其原因我们上一节已经有所讨论。
假设正在编译时键入“Ctrl-c”,此时编译器已经开始写文件“foo.o”,但是“Ctrl-c”产生的信号关闭了编译器。
这种情况下文件“foo.o”可能是不完整的,但这个内容不完整的“foo.o”文件的时间戳比源程序‘foo.c’的时间戳新。
如果在make收到终止信号后不删除文件“foo.o”而直接退出,那么下次执
行make时此文件被认为已是最新的而不会去重建它。
最后在链接生成终极目标时由于某一个.o文件的不完整,可能出现一堆令人难以理解的错误信息,或者产生了一个不正确的终极目标。
相反,可以在Makefile中将一个目标文件作为特殊目标“.PRECIOUS”的依赖,来取消make在重建这个目标时,在异常终止的情况下对这个目标文件的删除动作。
每一次在make在重建一个目标之前,都将首先判断该目标文件是否出现在特殊目标“.PRECIOUS”的依赖列表中,决定在终止信号发生时是否要删除这个目标文件。
不删除这种目标文件的原因可能是:
1.目标的重建动作是一个原子的不可被中断的过程;2.目标文件的存在仅仅为了记录其重建时间(不关心其内容无;3.这个目标文件必须一直存在来防止其它麻烦。
5.6make的递归执行
make的递归过程指的是:
在Makefile中使用“make”作为一个命令来执行本身或者其它makefile文件的过程。
递归调用在一个存在有多级子目录的项目中非常有用。
例如,当前目录下存在一个“subdir”子目录,在这个子目录中有描述此目录编译规则的makefile文件,在执行make时需要从上层目录(当前目录开始并完成它所有子目录的编译。
那么在当前目录下可以使用这样一个规则来实现对这个子目录的编译:
subsystem:
cdsubdir&&$(MAKE
其等价于规则:
subsystem:
$(MAKE-Csubdir
对这两个规则的命令进行简单说明,规则中“$(MAKE”是对变量“MAKE”(下一小节将详细讨论的引用(关于变量可参考第六章Makefile中的变量。
第一个规则命令的意思是:
进入子目录,然后在子目录下执行make。
第二个规则使用了make的“-C”选项,同样是首先进入子目录而后再执行make。
书写这样的规则对于我们来说应该不是什么大问题,但是其中有一些需要我们深入了解的东西。
首先需要了解它如何工作、上层make(在当前目录下运行的make进程和下层make(subdir目录下运行的make进程之间存在的联系。
也许会发现这两个规则的实现,使用伪目标更能提高效率。
在make的递归调用中,需要了解一下变量“CURDIR”,此变量代表make的工作目录。
当使用“-C”选项进入一个子目录后,此变量将被重新赋值。
总之,如果在Makefile中没有对此变量进行显式的赋值操作,那么它代表make的工作目录。
我们也可以在Makefile为这个变量赋一个新的值。
此时这变量将不再代表make的工作目录。
5.6.1变量MAKE
在使用make的递归调用时,在Makefile规则的命令行中应该使用变量“MAKE”来代替直接使
用“make”。
上一小节的例子应该这样来书写:
subsystem:
cdsubdir&&$(MAKE
变量“MAKE”的值是“make”。
如果其值为“/bin/make”那么上边规则的命令就为“cdsubdir&&/bin/make”。
这样做的好处是:
当我们使用一个其它版本的make程序时,可以保证最上层使用的make程序和其子目录下执行的make程序保持一致。
另外使用此变量的另外一个特点是:
当规则命令行中变量MAKE时,可以改变make的“-t”(“--
touch”,“-n”(“--just-print”和“-q”(“--question”命令行选项的效果。
它所实现的功能和在
规则中命令行首使用字符“+”的效果相同。
在规则的命令行中使用“make”代替了“$(MAKE”以后,上例子规则的命令行为:
“cdsubdir&&make”。
在我们执行“make-t”(“-t”选项用来更新所有目标的时间戳,而不执行任何规则的命令,结果是仅仅创建一个名为“subsystem”的文件,而不会进入到目录“subdir”去更新此目录下文件的时间戳。
我们使用“-t”命令行参数的初衷是对规则中的目标文件的时间戳进行更新。
而如果使“cdsubdir&&$(MAKE”作为规则的命令行,执行“make-t”就可以实现我们的初衷。
变量“MAKE”的这个特点是:
在规则的命令行中如果使用变量“MAKE”,标志“-t”、“-n”和“-q”在这个命令的执行中不起作用。
尽管这些选项是告诉make不执行规则的命令行,但包含变量“MAKE”的命令行除外,它们会被正常执行。
同时,执行make的命令行选项参数被通过一个变量“MAKEFLAGS”传递给子目录下的make程序。
例如,当使用make的命令行选项“-t”来更新目标的时间戳或者“-n”选项打印命令时,这些选项将会被赋值给变量“MAKEFLAGS”被传递到下一级的make程序中。
在下一级子目录中执行的make,这些选项会被附加作为make的命令行参数来执行,和在此目录下使用“make-t”或者“make-n”有相同的效果。
5.6.2变量和递归
在make的递归执行过程中,上层make可以明确指定将一些变量的定义通过环境变量的方式传递给
子make过程。
没有明确指定需要传递的变量,上层make不会将其所执行的Makefile中定义的变量传递给子make过程。
使用环境变量传递上层所定义的变量时,上层所传递给子make过程的变量定义不会覆盖
子make过程所执行makefile文件中的同名变量定义。
如果子make过程所执行Makefile中存在同名变量定义,则上层传递的变量定义不会覆盖子Makefile中定义的值。
就是说如果上层make传递的变量和子make所执行的Makefile中存在重复的变量定义,则以
子Makefile中的变量定义为准。
除非使用make的“-e”选项。
我们在本节第一段中提到,上层make过程要将所执行的Makefile中的变量传递给子make过程,需要明确地指出。
在GNUmake中,实现此功能的指示符是“export”。
当一个变量使用“export”进行声明后,变量和它的值将被加入到当前工作的环境变量中,以后在make执行的所有规则的命令都可以使用这个变量。
而当没有使用指示符“export”对任何变量进行声明的情况下,上层make只将那些已经初始化的环境变量(在执行make之前已经存在的环境变量和使用命令行指定的变量(如命令“makeCFLAGS+=-g”或者“make–eCFLAGS+=-g”传递给子make程序,通常这些变量由字符、数字和下划线组成。
需要注意的是:
有些shell不能处理那些名字中包含除字母、数字、下划线以外的其他字符的变量。
存在两个特殊的变量“SHELL”和“MAKEFLAGS”,对于这两个变量除非使用指示符“unexport”对它们进行声明,它们在整个make的执行过程中始终被自动的传递给所有的子make。
另外一个变
量“MAKEFILES”,如果此变量有值(不为空那么同样它会被自动的传递给子make。
在没有使用关键字“export”声明的变量,make执行时它们不会被自动传递给子make,因此下层Makefile中可以定义和上层同名的变量,不会引起变量定义冲突。
需要将一个在上层定义的变量传递给子make,应该在上层Makefile中使用指示符“export”对此变量进行声明。
格式如下:
exportVARIABLE...
当不希望将一个变量传递给子make时,可以使用指示符“unexport”来声明这个变量。
格式如下:
unexportVARIABLE...
以上两种格式,指示符“export”或者“unexport”的参数(变量部分,如果它是对一个变量或者函数的引用,这些变量或者函数将会被立即展开。
并赋值给export或者unexport的变量(关于变量展开的过程可参考第六章Makefile中的变量。
例如:
Y=Z
exportX=$(Y
其实就是“exportX=Z”。
export时对变量进行展开,是为了保证传递给子make的变量值有效(使用当前Makefile中定义的变量值。
“export”更方便的用法是在定义变量的同时对它进行声明。
看下边的几个例子:
1.
exportVARIABLE=value
等效于:
VARIABLE=value
exportVARIABLE
2.
exportVARIABLE:
=value
等效于:
VARIABLE:
=value
exportVARIABLE