第五章:规则的命令 -------------------------------------------------------------------------------- 规则的命令由一些shell命令行组成,他们被一条一条的执行。规则中除了第一条紧跟在依赖列表之后使用分号隔开的命令以外,其它的每一行命令行必需一 [Tab]字符开始。多个命令行之间可以有空行和注释行(所谓空行,就是没有包含任何字符的一行。如果以[Tab]键开始而其后没有命令的行,此行不是空 行。是空命令行),在执行规则时他们将被忽略。 可能用户使用了多个不同的shell。但是在make处理Makefile过程是,如果没有明确指定,那么对所有规则中命令行的解析使用“/bin/sh”来完成。 使用的shell决定了规则中的命令的语法和处理机制。当使用默认的“/bin/sh”时,命令中字符“#”到行末的内容被认为是注释。当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注视处理。 另外在make解析makefile文件时,对待注释也是采用同样的处理方式。我们的shell脚本也一样。 5.1 命令回显 通常,make在执行命令行之前会把要执行的命令行进行输出。我们称之为“回显”,就好像我们输入命令执行一样。 如果要执行的命令行以字符“@”开始,则make在执行时这个命令就不会被回显。典型的用法是我们在使用“echo”命令输出一些信息时。如: @echo 开始编译XXX模块...... 当make执行时,将输出“开始编译XXX模块......”这个信息。如果在命令行之前没有字符“@”,那么,make的输出就是: echo编译XXX模块...... 编译XXX模块...... 另外,如果使用make的命令行参数“-n”或“--just-print”,那么make执行时只显示所要执行的命令,但不会真正的去执行这些命令。只 有在这种情况下make才会打印出所有make需要执行的命令,其中也包括了使用“@”字符开始的命令。这个选项对于我们调试Makefile非常有用, 使用这个选项我们可以按执行顺序打印出Makefile中所有需要执行的命令。 而make参数“-s”或“--slient”则是禁止所有执行命令的显示,就好像所有的命令行均使用“@”开始一样。在Makefile中使用没有依赖 的特殊目标“.SILENT”也可以禁止命令的回显,但是它的缺点是不如“@”灵活。因此我们在书写Makefile时,推荐使用“@”来控制命令的回 显。 5.2 命令的执行 规则中,当目标需要被重建时。此规则所定义的命令将会被执行,如果是多行命令,那么make就为每一行命令使用一个独立的子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 并发执行命令 GUN make可以同时执行多条命令。通常情况下,一个时刻只有一个命令在执行,下一个命令在当前命令执行完成之后才能够被执行。不过可以通过make的命令行 选项“-j”或者“--job”来告诉make在同一时刻可以允许多条命令同时被执行(注意,在MS-DOS中此选项无效,因为它是单任务操作系统)。 如果选项“-j”之后存在一个整数,其含义是告诉make在同一时刻可允许同时执行命令的数目。这个数字被称为“job slots”。当“-j”选项之后没有出现一个数字时,那么同一时刻执行的命令数目没有要求。使用默认的“job slots”,其值为1。表示make将串行的执行规则的命令(同一时刻只能有一条命令被执行)。 并行执行命令所带来的问题是显而易见: 1. 同一时刻的多个被执行的命令同时输出,造成输出到终端的信息的交替,显得凌乱。当出现问题是很难命令执行失败时很难根据命令的输出信息来定位错误。 2. 在同一时刻可能会存在多个命令执行进程读取标准输入,但是对于标注输入来设备来说,在同一时刻只能有一个进程访问输入设备。就是说在某个时间点,make 只能保证这个时刻正在执行的进程中的一个进程读取标准输入流,而其它的进程的标准输入流将变得无效。因此当同一时刻存在多个执行命令的进程需要读取标准输 入流时其它的将会出输入流无效导致致命错误(通常此进程会得到操作系统的管道破裂信号而被终止)。 这是因为:执行中的命令在什么时候会读取标准输入流(终端输入或重定向的标准输入)是不可预测的。而得到标准输入的顺序总是按照先来先获得的原则。那个命 令首先被执行,那么它就可以首先得到标准输入设备。而其它后续需要获取标准输入设备的命令执行进程,由于不能得到标准输入而产生致命错误。再 Makefile规则中如果存在很多命令需要读取标准输入设备,而它们又被允许并行执行时,就会出现这样的错误。 对这个问题的解决。我们可以修改Makefile的规则命令使之在执行过程中不使用标准输入设备。当然也可以实现在只存在一个命令在执行时会访问标准输入流的Makefile。 3. 会导致make的递归调用出现问题。可参考 5.6 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已经知道因为这个错误而无法完成终极目标的重建,但还是继续完成其它后续的 依赖文件的重建。直到执行最后链接时才错误退出。 一般“-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时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”)命令行选项的效果。它所实现的功能和在规则中命令行首使用字符“+”的效果相同(可参考 9.3 替代命令的执行 一节)。 在规则的命令行中使用“make”代替了“$(MAKE)”以后,上例子规则的命令行为:“cd subdir && make”。在我们执行“make -t”(“-t”选项用来更新所有目标的时间戳,而不执行任何规则的命令,参考 9.7 make的命令行选项 一节),结果是仅仅创建一个名为“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中可以定义和上层同名的变量,这样不会引起变量定 义冲突。 上层Makefile中定义的某一个变量需要传递给子make时,应该在上层Makefile中使用指示符“export”对此变量进行声明。格式如下: export VARIABLE ... 当不希望将一个变量传递给子make时,可以使用指示符“unexport”来声明这个变量。格式如下: unexport VARIABLE ... 在以上的两种格式,指示符“export”或者“unexport”的参数(变量部分),如果它是对一个变量或者函数的引用,这些变量或者函数将会被立即展开。并赋值给export或者unexport的变量。例如: Y = Z export X=$(Y)” 等价于“export X=Z”。在这里进行展开是为了保证传递给子make的此变量的值有效。 “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”来声明这个变量,这 个被声明的变量就不会被传递给子make。使用“export”将所有定义的变量传递给子Makefile时,那些名字中包含其它字符(除字母、数字和下 划线以外的字符)的变量可能不会被传递给子make,对这类特殊命名的变量传递需要明确的使用“export”指示符对它进行声明。 需要说明的是:单独使用“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进程的。其实真正的命令行中的 变量定义 是通过另外一个变量“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”的值,更不要把它设置为系统的环境变量来使 用。否则可能会产生一些奇怪甚至让你感到不解的现象。 1.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”来定义这样一组命令,同时用一个变量(作为一个变量,不能和 Makefile中其它常规的变量命名出现冲突)来代表这一组命令。通常我们把使用“define”定义的一组命令称为一个命令包。定义一个命令包的语法 以“define”开始,以“endef”结束,例如: define run-yacc yacc $(firstword $^) mv y.tab.c $@ endef 这里,“run-yacc”是这个命令包的名字。在“define”和“endef”之间的命令就是命令包的主体。需要说明的是:使用“define”定 义的命令包中,命令体中变量和函数的引用不会展开。命令体中所有的内容包括“$”、“(”、“)”等都是变量“run-yacc”的定义。它和c语言中宏 的使用方式一样。 例子中,命令包中第一个命令是对引用它所在规则中的第一个依赖文件(函数“firstword”,可参考 8.2 文本处理函数 一节)运行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”指定的命令。关于隐含规则可参考 第10章 使用隐含规则)。这一点和伪目标有相同之处。在使用空命令的目标时,需要说明的是:实现一个没有实际文件的目标,这个目标只是作为一个标签,来完成它的依 赖文件的重建动作。实现这个目的,首先应该想到伪目标而不是空命令目标。因为一个实际不存在的目标文件的依赖文件,可能不会被正确的重建。 所以,对于空命令规则,最好不要给它指定依赖文件。避免特殊情况下产生错误的情况。定义一个空命令规则,建议使用上例的格式。 |
联系客服
微信登录中...
请勿关闭此页面