第五章:规则的命令

 

第五章:规则的命令


 

规则的命令由一些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

cd bar; gobble lose > ../foo

 

如果希望把一个完整的shell命令行书写在多行上,需要使用反斜杠(\)来对处于多行的命令进行连接,表示他们是一个完整的shell命令行。例如上例我们以也可以这样书写:

foo : bar/lose

cd bar;  \

gobble lose > ../foo

 

make对所有规则命令的解析使用环境变量“SHELL”所指定的那个程序,在GNU make中,默认的程序是“/bin/sh”。

不像其他绝大多数变量,它们的值可以直接从同名的系统环境变量那里获得。make的环境变量“SHELL”没有使用系统环境变量的定义。因为系统环境变量“SHELL”指定那个程序被用来作为用户和系统交互的接口程序,它对于不存在直接交互过程的make显然不合适。在make的环境变量中“SHELL”会被重新赋值;它作为一个变量我们也可以在Makefile中明确地给它赋值(指出解释程序的名字,当明确指定时需要使用完整的路径名。如“/bin/sh”),变量“SHELL”的默认值是“/bin/sh”。

(在MS-DOS下有些不同, MS-DOS不存在SHELL环境变量。这里不对MS-DOS下make进行介绍,有兴趣地可以自行参考info make关于此部分的描述)

5.3      并发执行命令

GNU make支持同时执行多条命令。通常情况下,同一时刻只有一个命令在执行,下一个命令只有在当前命令执行完成之后才能够开始执行。不过可以通过make的命令行选项“-j”或者“--job”来告诉make在同一时刻可以允许多条命令同时被执行(注意,在MS-DOS中此选项无效,因为它是单任务操作系统)。

如果选项“-j”之后存在一个整数,其含义是告诉make在同一时刻可允许同时执行命令的数目。这个数字被称为“job slots”。当“-j”选项之后没有出现一个数字时,那么同一时刻执行的命令数目没有要求。使用默认的“job slots”,值为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”选项一般后边需要跟一个浮点数。如:

-l 2.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执行失败之后使用“make clean”来清除已经重建的所有目标,之后再执行make。我们也可以让make自动完成这个动作,我们只需要在Makefile中定义一个特殊的目标“.DELETE_ON_ERROR”。但是这个做法存在不兼容。推荐的做法是:在make执行失败时,修改错误之后执行make之前,使用“make clean”明确的删除第一次错误重建的所有目标

本节的最后,需要说明的是:虽然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.6      make的递归执行

make的递归过程指的是:在Makefile中使用“make”作为一个命令来执行本身或者其它makefile文件的过程。递归调用在一个存在有多级子目录的项目中非常有用。例如,当前目录下存在一个“subdir”子目录,在这个子目录中有描述此目录编译规则的makefile文件,在执行make时需要从上层目录(当前目录)开始并完成它所有子目录的编译。那么在当前目录下可以使用这样一个规则来实现对这个子目录的编译:

subsystem:

cd subdir && $(MAKE)

 

其等价于规则:

 

subsystem:

$(MAKE) -C subdir

 

对这两个规则的命令进行简单说明,规则中“$(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:

cd subdir && $(MAKE)

 

变量“MAKE”的值是“make”。如果其值为“/bin/make”那么上边规则的命令就为“cd subdir && /bin/make”。这样做的好处是:当我们使用一个其它版本的make程序时,可以保证最上层使用的make程序和其子目录下执行的make程序保持一致。

另外使用此变量的另外一个特点是:当规则命令行中变量MAKE时,可以改变make的“-t”(“--touch”),“-n”(“--just-print”)和“-q”(“--question”)命令行选项的效果。它所实现的功能和在规则中命令行首使用字符“+”的效果相同。

在规则的命令行中使用“make”代替了“$(MAKE)”以后,上例子规则的命令行为:“cd subdir && make”。在我们执行“make -t”(“-t”选项用来更新所有目标的时间戳,而不执行任何规则的命令),结果是仅仅创建一个名为“subsystem”的文件,而不会进入到目录“subdir”去更新此目录下文件的时间戳。我们使用“-t”命令行参数的初衷是对规则中的目标文件的时间戳进行更新。而如果使“cd subdir && $(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过程,需要明确地指出。在GNU make中,实现此功能的指示符是“export”。当一个变量使用“export”进行声明后,变量和它的值将被加入到当前工作的环境变量中,以后在make执行的所有规则的命令都可以使用这个变量。而当没有使用指示符“export”对任何变量进行声明的情况下,上层make只将那些已经初始化的环境变量(在执行make之前已经存在的环境变量)和使用命令行指定的变量(如命令“make CFLAGS +=-g”或者“make –e CFLAGS +=-g”)传递给子make程序,通常这些变量由字符、数字和下划线组成。需要注意的是:有些shell不能处理那些名字中包含除字母、数字、下划线以外的其他字符的变量。

存在两个特殊的变量“SHELL”和“MAKEFLAGS”,对于这两个变量除非使用指示符“unexport”对它们进行声明,它们在整个make的执行过程中始终被自动的传递给所有的子make。另外一个变量“MAKEFILES”,如果此变量有值(不为空)那么同样它会被自动的传递给子make。在没有使用关键字“export”声明的变量,make执行时它们不会被自动传递给子make,因此下层Makefile中可以定义和上层同名的变量,不会引起变量定义冲突。

需要将一个在上层定义的变量传递给子make,应该在上层Makefile中使用指示符“export”对此变量进行声明。格式如下:

 

export VARIABLE ...

 

当不希望将一个变量传递给子make时,可以使用指示符“unexport”来声明这个变量。格式如下:

 

unexport VARIABLE ...

 

以上两种格式,指示符“export”或者“unexport”的参数(变量部分),如果它是对一个变量或者函数的引用,这些变量或者函数将会被立即展开。并赋值给export或者unexport的变量(关于变量展开的过程可参考 第六章 Makefile中的变量)。例如:

 

Y = Z

export X=$(Y)

 

其实就是“export X=Z”。export时对变量进行展开,是为了保证传递给子make的变量值有效(使用当前Makefile中定义的变量值)。

“export”更方便的用法是在定义变量的同时对它进行声明。看下边的几个例子:

1.         

export VARIABLE = value

 

 

等效于:

 

VARIABLE = value

export VARIABLE

 

2.         

 

export VARIABLE := value

等效于:

 

VARIABLE := value

export VARIABLE

 

3.         

export VARIABLE += value

 

等效于:

 

VARIABLE += value

export VARIABLE

 

我们可以看到,其实在Makefile中指示符“export”和“unexport”的功能和在shell下功能基本相同。

一个不带任何参数的指示符“export”指示符:

export

 

含义是将此Makefile中定义的所有变量传递给子make过程。如果不需要传递其中的某一个变量,可以单独使用指示符“unexport”来声明这个变量。使用“export”将所有定义的变量传递给子Makefile时,那些名字中包含其它字符(除字母、数字和下划线以外的字符)的变量可能不会被传递给子make,对这类特殊命名的变量传递需要明确的使用“export”指示符对它进行声明。虽然不带任何参数的“export”指示符具有特殊的含义,但是一个不带任何参数的“unexport”指示符却是没有任何意义的,它不会对make的执行过程(变量的传递)产生任何影响。

需要说明的是:单独使用“export”来导出所有变量的行为是老版本GNU make所默认的。但是在新版本的GNU make中取消了这一默认的行为。因此在编写和老版本GNU make兼容的Makefile时,需要使用特殊目标“.EXPORT_ALL_VARIABLES”来代替“export”,此特殊目标的功和不带参数的“export”相同。它会被老版本的make忽略,只有新版本的make能够识别这个特殊目标。这是因为,老版本的GNU make不能识别和解析指示符“export”。为了和老版本兼容我们可以这样声明一些变量:

 

.EXPORT_ALL_VARIABLES:

VARIABLE1=var1

VARIABLE2=var2

 

这对不同版本的make来说都是兼容的,其含义是将特殊目标“.EXPORT_ALL_VARIABLES”依赖中的所有变量全部传递给子make。

和指示符“export”相似,也可以使用单独的“unexport”指示符来禁止一个变量的向下传递。这一动作是现行版本make所默认的,因此我们就没有必要在上层的Makefile中使用它。在多级的make递归调用中,可以在中间的Makefile中使用它来限制上层传递来的变量再向下传递。需要明确的是,不能使用“export”或者“unexport”来实现对命令中使用变量的控制功能。就是说,不能做到用这两个指示符来限制某个(些)变量在执行特定命令时有效,而对于其它的命令则无效。在Makefile中,最后一个出现的指示符“export”或者“unexport”决定整个make运行过程中变量是否进行传递。

在多级递归调用的make执行过程中。变量“MAKELEVEL”代表了调用的深度。在make一级级的执行过程中变量“MAKELEVEL”的值不断的发生变化,通过它的值我们可以了解当前make递归调用的深度。最上一级时“MAKELEVEL”的值为“0”、下一级时为“1”、再下一级为“2”.......例如:

Main目录下的Makefile清单如下:

#maindir Makefile

………

………

.PHONY :test

test:

       @echo “main makelevel = $(MAKELEVEL)”

       @$(MAKE) –C subdir dislevel

 

#subdir Makefile

………..

………..

.PHONY : test

test :

       @echo “subdir makelevel = $(MAKELEVEL)”

 

在maindir 目录下执行“make test”。将显式如下信息:

main makelevel = 0

make[1]: Entering directory `/…../ subdir '

subdir makelevel = 1

make[1]: Leaving directory `/…../ subdir '

 

在主控的Makefile中MAKELEVEL 为“0”,而在subdir的Makefile中,MAKELEVEL为“1”。

这个变量主要用在条件测试指令中。例如:我们可以通过测试此变量的值来决定是否执行递归方式的make调用或者其他方式的make调用。我们希望一个子目录必须被上层make调用才可以执行此目录下的Makefile,而不允许直接在此目录下执行make。我们可以这样实现:

 

.......

ifeq ($(MAKELEVEL),0)

all : msg

else

all : other  

endif

 

……

…...

 

msg:

@echo ”Can not make in this directory!”

……

……

 

当在包含次条件判断的Makefile所在的目录下执行make时,将会得到提示“Can not make in this directory!”。

5.6.3       命令行选项和递归

在make的递归执行过程中。最上层(可以称之为主控)make的命令行选项“-k”、“-s”等会被自动的通过环境变量“MAKEFLAGS”传递给子make进程。传递过程中变量“MAKEFLAGS”的值会被主控make自动的设置为包含执行make时的命令行选项的字符串。如果在执行make时通过命令行指定了“-k”和“-s”选项,那么“MAKEFLAGS”的值会被自动设置为“ks”。子make进程在处理时,会把此环境变量的值作为执行的命令行参数,因此子make过程同样也会有“-k”和“-s”这两个命令行选项。

同样,执行make时命令行中给定的一个变量定义(如“make CFLAGS+=-g”),此变量和它的值(CFLAGS+=-g)也会借助环境变量“MAKEFLAGS”传递给子make进程。可以借助make的环境变量“MAKEFLAGS” 传递我们在主控make所使用的命令行选项给子make进程。需要注意的是有几个特殊的命令行选项例外,他们是:“-C”、“-f”、“-o”和“-W”。这些命令行选项是不会被赋值给变量“MAKEFLAGS”的。

Make命令行选项中一个比较特殊的是“-j”选项。在支持这个选项的操作系统上,如果给它指定了一个数值“N”(多任务的系统unix、Linux支持,MS-DOS不支持),那么主控make和子make进程会在执行过程中使用通信机制来限制系统在同一时刻(包括所有的递归调用的make进程,否则,将会导致make任务的数目数目无法控制而使别的任务无法到的执行)所执行任务的数目不大于“N”。另外,当使用的操作系统不能支持make执行过程中的父子间通信,那么无论在执行主控make时指定的任务数目“N”是多少,变量“MAKEFLAGS”中选项“-j”的数目会都被设置为“1”,通过这样来确保系统的正常运转。

执行多级的make调用时,当不希望传递“MAKEFLAGS”的给子make时,需要在调用子make是对这个变量进行赋空。例如:

 

subsystem:

cd subdir && $(MAKE) MAKEFLAGS=

 

此规则取消了子make执行时对父make命令行选项的继承(将变量“MAKEFLAGS”的值赋为空)。

执行make时可以通过命令行来定义一个变量,像上例中的那样;前边已经提到过,这种变量是借助环境“MAKEFLAGS”来传递给多级调用的子make进程的。其实真正的命令行中的 变量定义 是通过另外一个变量“MAKEOVRRIDES”记录的,在变量“MAKEFLAGS”的定义中引用了此变量,所以命令行中的变量定义被记录在环境变量“MAKEFLAGS”中被传递下去。当不希望上层make在命令行中定义的变量传递给子make时,可以在上层Makefile中把“MAKEOVERRIDES”赋空(MAKEOVERRIDES=)。但是这种方式通常很少使用,建议非万不得已您还是最好不使用这种方式(为了和POSIX2.0兼容,当Makefile中出现“.POSIX”这个特殊的目标时,在上层Makefile中修改变量“MAKEOVERRIDES”对子make不会产生任何影响)。另外,在一些系统中环境变量值的长度存在一个上限,一次当“MAKEFLAGS”的值超过一定长度时,执行过程可能会出现类似“Arg list too long”的错误提示。

历史原因,在make中也存在另外一个和“MAKEFLAGS”相类似的变量“MFLAGS”。现行版本中保留此变量的原因是为了和老版本兼容。和“MAKEFLAGS”不同点是:1. 此变量在make的递归调用时不包含命令行选项中的变量定义部分(就是说此变量的定义没有包含对“MAKEOVERRIDES”的引用);2. 此变量的值(除为空的情况)是以“-”开始的,而“MAKEFLAGS”的值只有在长命令选项格式(如:“--warn-undefined-variables”)时才以“-”开头。传统的此变量一般被明确的使用在make递归调用时的命令中。像下边那样:

 

subsystem:

cd subdir && $(MAKE) $(MFLAGS)

 

在现行的make版本中,变量“MFLAGS”已经成为一个多余部分。在书写和老版本make兼容的Makefile时可能需要这个变量。当然它在目前的版本上也能够正常的工作。

在某些特殊的场合,可能需要为所有的make进程指定一个统一的命令行选项。比如说需要给所有的运行的make指定“-k”选项。实现这个目的,我们可以在执行make之前设置一个系统环境变量(存在于当前系统的环境中)“MAKEFLAGS=k”,或者在主控Makefile中将它的值赋为“k”。注意:不能通过变量“MFLAGS”来实现。

make在执行时,首先将会对变量“MAKEFLAGS”的值(系统环境中或者在Makefile中设置的)进行分析。当变量的值不是以连字符(“-”)开始时,将变量的值按字分开,字之间使用空格分开。将这些字作为命令行的选项对待(除了选项“-C”、“-f”、“-h”、“-o”和“-W”以及他们的长格式,如果其中包含无效的选项不会提示错误)。

最后需要说明的是:将“MAKEFLAGS”设置为系统环境变量的做法是不可取的!因为这样一旦将一些调试选项或者特殊选项作为此变量值的一部分,在执行make时,会对make的正常执行产生潜在的影响。例如如果变量“MAKEFLAGS”中包含选项“t”、“n”、“q”这三个的任何一个,当执行make的结果可能就不是你所要的。建议大家最好不要随便更改“MAKEFLAGS”的值,更不要把它设置为系统的环境变量来使用。否则可能会产生一些奇怪甚至让你感到不解的现象。

5.6.4  -w选项

在多级make的递归调用过程中,选项“-w”或者“--print-directory”可以让make在开始编译一个目录之前和完成此目录的编译之后给出相应的提示信息,方便开发人员跟踪make的执行过程。例如,在目录“/u/gnu/make”目录下执行“make -w”,将会看到如下的一些信息:

在开始执行之前我们将看到:

 

make: Entering directory `/u/gnu/make'.

 

而在完成之后我们同样将会看到:

 

make: Leaving directory `/u/gnu/make'.

 

通常,选项“-w”会被自动打开。在主控Makefile中当如果使用“-C”参数来为make指定一个目录或者使用“cd”进入一个目录时,“-w”选项会被自动打开。主控make可以使用选项“-s”(“--slient”)来禁止此选项。另外,make的命令行选项“--no-print-directory”,将禁止所有关于目录信息的打印。

5.7      定义命令包

书写Makefile时,可能有多个规则会使用相同的一组命令。就像c语言程序中需要经常使用到函数“printf”。这时我们就会想能不能将这样一组命令进行类似c语言函数一样的封装,以后在我们需要用到的地方可以通过它的名字(c语言中的函数名)来对这一组命令进行引用。这样就可减少重复工作,提高了效率。在GNU make中,可以使用指示符“define”来完成这个功能(关于指示符“define”)。通过“define”来定义这样一组命令,同时用一个变量(作为一个变量,不能和Makefile中其它常规的变量命名出现冲突)来代表这一组命令。通常我们把使用“define”定义的一组命令称为一个命令包。定义一个命令包的语法以“define”开始,以“endef”结束,例如:

 

define run-yacc

yacc $(firstword $^)

mv y.tab.c $@

endef

 

这里,“run-yacc”是这个命令包的名字。在“define”和“endef”之间的命令就是命令包的主体。需要说明的是:使用“define”定义的命令包中,命令体中变量和函数的引用不会展开。命令体中所有的内容包括“$”、“(”、“)”等都是变量“run-yacc”的定义。它和c语言中宏的使用方式一样。关于变量可参考 第六章 Makefile中的变量

例子中,命令包中第一个命令是对引用它所在规则中的第一个依赖文件运行yacc程序。yacc程序总是生成一个命名为“y.tab.c”的文件。第二行的命令就是把这个文件名改为规则目标的名字。

定义了这样一个命令包后,后续应该如何使用它?前面已经提到,命令包是使用一个变量来表示的。因此我们就可以按使用变量的方式来使用它。当在规则的命令行中使用这个变量时,命令包所定义的命令体就会对它进行替代。由于使用“define”定义的变量属于递归展开式变量,因此,命令包中所有命令中对其它变量的引用,在规则被执行时会被完全展开。例如这样一个规则:

 

foo.c : foo.y

$(run-yacc)

 

此规则在执行时,我们来看一下命令包中的变量的替换过程:1. 命令包中的“$^”会被“foo.y”替换;2. “$@”被“foo.c”替换。大家应该对“$<”和“$@”不陌生吧。

当在一个规则中引用一个已定义的命令包时,命令包中的命令体会被原封不动的展开在引用它的地方(和 c语言中的宏一样)。这些命令就成为规则的命令。因此我们也可在定义命令包时使用前缀来控制单独的一个命令行(例如“@”,“-”和“+”)。例如:

 

define frobnicate

     @echo "frobnicating target $@"

     frob-step-1 $< -o $@-step-1

     frob-step-2 $@-step-1 -o $@

endef

 

此命令包的第一行命令执行前不会被回显,其它的命令在执行前都被回显。

另一方面,如果一个规则在引用此命令包之前使用了控制命令的前缀字符。那么这个前缀字符将会被添加到命令包定义的每一个命令行之中。例如:

frob.out: frob.in

@$(frobnicate)

 

这个规则执行时不会回显任何要执行的命令。

5.8      空命令

有时可能存在这样的一个需求,需要定义一个什么也不做的规则(不需要任何执行的命令)。前面已经有过这样的用法。这样的规则,只有目标文件(可以存在依赖文件)而没有命令行。像这样定义:

 

target: ;

 

这就是一个空命令的规则,为目标“target”定义了一个空命令。也可以使用独立的命令行格式来定义,需要注意的是独立的命令行必须以[Tab]字符开始。一般在定义空命令时,建议不使用命令行的方式,因为看起来空命令行和空行在感觉上没有区别。

大家会奇怪为什么要定义一个没有命令的规则。其唯一的原因是,空命令行可以防止make在执行时试图为重建这个目标去查找隐含命令(包括了使用隐含规则中的命令和“.DEFAULT”指定的命令。关于隐含规则可参考 第十章 使用隐含规则)。这一点它和伪目标有相同之处。使用空命令的目标时,需要注意:如果需要实现一个不是实际文件的目标,我们只是需要通过使用这个目标来完成对它所依赖的文件的重建动作。首先应该想到伪目标而不是空命令目标。因为一个实际不存在的目标文件的依赖文件,可能不会被正确重建。

因此,对于空命令规则,最好不要给它指定依赖文件。避免特殊情况下产生错误的情况。定义一个空命令规则,建议使用上例的格式。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值