Chinaunix首页 | 论坛 | 博客
  • 博客访问: 15612
  • 博文数量: 5
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 10
  • 用 户 组: 普通用户
  • 注册时间: 2014-01-25 14:16
文章分类
文章存档

2014年(5)

我的朋友

分类: 嵌入式

2014-04-01 16:52:03

原文地址:makefile 规则的命令(上) 作者:LYZC11

规则的命令

 

1         规则中书写命令

规则的命令由一些shell命令行组成,他们被一条一条的执行。规则中除了第一条紧跟在依赖列表之后使用分号隔开的命令以外,其它的每一行命令行必需一[Tab]字符开始。多个命令行之间可以有空行和注释行(所谓空行,就是没有包含任何字符的一行。如果以[Tab]键开始而其后没有命令的行,此行不是空行。是空命令行),在执行规则时他们将被忽略。

可能用户使用了多个不同的shell。但是在make处理Makefile过程是,如果没有明确指定,那么对所有规则中命令行的解析使用“/bin/sh”来完成。

使用的shell决定了规则中的命令的语法和处理机制。当使用默认的“/bin/sh”时,命令中字符“#”到行末的内容被认为是注释。当然了“#”可以不在此行的行首,此时“#”之前的内容不会被作为注视处理。

另外在make解析makefile文件时,对待注释也是采用同样的处理方式。我们的shell脚本也一样。

1.1        命令回显

通常,make在执行命令行之前会把要执行的命令行进行输出。我们称之为“回显”,就好像我们输入命令执行一样。

如果要执行的命令行以字符“@”开始,则make在执行时这个命令就不会被回显。典型的用法是我们在使用“echo”命令输出一些信息时。如:

 

@echo 开始编译XXX模块......

 

make执行时,将输出“开始编译XXX模块......”这个信息。如果在命令行之前没有字符“@”,那么,make的输出就是:

 

echo编译XXX模块......

编译XXX模块......

 

另外,如果使用make的命令行参数“-n”或“--just-print”,那么make执行时只显示所要执行的命令,但不会真正的去执行这些命令(可参考 8.7 make的命令行选项 一节)。只有在这种情况下make才会打印出所有make需要执行的命令,其中也包括了使用“@”字符开始的命令。这个选项对于我们调试Makefile非常有用,使用这个选项我们可以按执行顺序打印出Makefile中所有需要执行的命令。

make参数“-s”或“--slient”则是禁止所有执行命令的显示,就好像所有的命令行均使用“@”开始一样。在Makefile中使用没有依赖的特殊目标“.SILENT”也可以禁止命令的回显(可参考 3.9 Makefile的特殊目标 一节),但是它的缺点是不如“@”灵活。因此我们在书写Makefile时,推荐使用“@”来控制命令的回显。

1.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-DOSmake的方法省略了,有兴趣地可以自行参考info make

1.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的递归调用出现问题。可参考 4.6 make的递归执行 一节。

Make在执行一个命令时,如果某一条命令执行失败(被一个信号中止,或非零退出),且该条命令产生的错误不可忽略(可参考 4.4 命令执行的错误 一节),那么其它的用于重建同一目标的命令执行将会被终止。此种情况下,如果make没有使用“-k”或“--keep-going”选项(可参考 8.7 make的命令行选项 一节),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就不会在其他任务完成之前启动任何任务。缺省情况下没有负荷限制。

1.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 ”等等。

1.5        中断make的执行

make在执行命令时如果收到一个致命信号(结束make),make将会删除命令重建的规则目标文件。其依据是此目标文件的当前时间戳是否和make开始时的时间戳相同。

删除这个目标文件的目的是为了确保下一次make时目标文件能够被正确重建。其原因我们上一节已经有所讨论。假设正在编译文件是键入“Ctrl-c”,在这时编译器已经开始写文件“foo.o”,但是“Ctrl-c”产生的信号关闭了编译器。这种情况时文件“foo.o”可能是不完整的,但这个内容不完整的“foo.o”文件的时间戳比源程序‘foo.c’的时间戳新。如果在make收到终止信号后不删除文件“foo.o”而直接退出,那么下次执行makemake将认为该文件已是最新而不会去重建文件它。最后在链接程序生成终极目标时可能由于某一个.o文件的不完整,导致出现一些奇怪的令人难以理解的错误信息。

同时,我们可以在Makefile中将一个目标文件作为特殊目标“.PRECIOUS”的依赖,这样可以防止make在重建这个目标时异常终止时对这个目标文件的删除动作。每次make在重建一个目标之前,首先判断该目标文件是否出现在特殊目标“.PRECIOUS”的依赖列表中,决定在终止信号发生时需不需要删除这个目标文件。不删除这种目标文件的原因可能是:1. 目标的重建动作是一个原子的不可被中断的过程;2. 目标文件的存在仅仅为了记录其重建时间(不关心其内容无);3. 这个目标文件必须一直存在来防止其它麻烦。

1.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进程)和下层makesubdir目录下运行的make进程)之间存在的联系。也许会发现这两个规则的实现,使用伪目标更能提高效率(可参考 3.6 Makefile伪目标 一节)。

make的递归调用中,需要了解变量“CURDIR”,此变量代表了make当前的工作路径。如果使用“-C”选项进入一个子目录后,此变量将被重新赋值。总之,如果在Makefile中没有对此变量进行显式的赋值操作,那么它代表make的当前工作目录。我们也可以在Makefile为这个变量赋一个新的值。此时这变量将不再代表make的工作目录。

1.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”)命令行选项的效果。它所实现的功能和在规则中命令行首使用字符“+”的效果相同(可参考 8.3 替代命令的执行 一节)。

在规则的命令行中使用“make”代替了“$(MAKE)”以后,上例子规则的命令行为:“cd subdir && make”。在我们执行“make -t”(“-t”选项用来更新所有目标的时间戳,而不执行任何规则的命令,参考 8.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”有相同的效果。

1.6.2       变量和递归

make的递归执行过程中,上层make可以明确指定将一些变量的定义通过环境变量的方式传递给子make过程。没有明确指定需要传递的变量,上层make不会将其所执行的Makefile中定义的变量传递给子make过程。使用环境变量传递上层所定义的变量时,上层所传递给子make过程的变量定义不会覆盖子make过程所执行makefile文件中的同名变量定义。

如果子make过程所执行Makefile中存在同名变量定义,则上层传递的变量定义不会覆盖子Makefile中定义的值。就是说如果上层make传递的变量和子make所执行的Makefile中存在重复的变量定义,则以子Makefile中的变量定义为准。除非使用make的“-e”选项(参考 8.7 make的命令行选项 一节)。

我们在本届的第一段中提到,当上层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的变量(关于变量展开的过程可参考 5 Makefile中的变量)。例如:

 

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 '

 

在主控的MakefileMAKELEVEL 为“0”,而在subdirMakefile中,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!

 

阅读(6601) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~