【六、深度解析Makefile】工程文件编译链接的“规则制定者”:带你走进 makefile 的世界(三)

简介: 【六、深度解析Makefile】工程文件编译链接的“规则制定者”:带你走进 makefile 的世界

(6)特殊目标

目标是规则中要生成的目标,在一个 makefile 中,至少要有一个最终目标。但是,目标是多种多样的,甚至有一些目标是不需要实际生成,比如前面说过的伪目标。下面介绍各种类型的目标。

① 特殊目标

名称 功能
.PHONY: 这个目标的所有依赖被作为伪目标。伪目标是这样一个目标:当使用 make 命令行指定此目标时,这个目标所在的规则定义的命令、无论目标文件是否存在都会被无条件执行。
.SUFFIXES: 这个目标的所有依赖指出了一系列在后缀规则中需要检查的后缀名。
.DEFAULT: Makefile 中,这个特殊目标所在规则定义的命令,被用在重建那些没有具体规则的目标,就是说一个文件作为某个规则的依赖,却不是另外一个规则的目标时,make 程序无法找到重建此文件的规则,这种情况就执行 “.DEFAULT” 所指定的命令。
.PRECIOUS: 这个特殊目标所在的依赖文件在 make 的过程中会被特殊处理:当命令执行的过程中断时,make 不会删除它们。而且如果目标的依赖文件是中间过程文件,同样这些文件不会被删除。
.INTERMEDIATE: 这个特殊目标的依赖文件在 make 执行时被作为中间文件对待。没有任何依赖文件的这个目标没有意义。
.SECONDARY: 这个特殊目标的依赖文件被作为中过程的文件对待。但是这些文件不会被删除。这个目标没有任何依赖文件的含义是:将所有的文件视为中间文件。
.IGNORE: 这个目标的依赖文件忽略创建这个文件所执行命令的错误,给此目标指定命令是没有意义的。当此目标没有依赖文件时,将忽略所有命令执行的错误。
.DELETE_ON_ERROR: 如果在 Makefile 中存在特殊的目标 “.DELETE_ON_ERROR” ,make 在执行过程中,荣国规则的命令执行错误,将删除已经被修改的目标文件。
.LOW_RESOLUTION_TIME: 这个目标的依赖文件被 make 认为是低分辨率时间戳文件,给这个目标指定命令是没有意义的。通常的目标都是高分辨率时间戳。
.SILENT: 出现在此目标 “.SILENT” 的依赖文件列表中的文件,make 在创建这些文件时,不打印出此文件所执行的命令。同样,给目标 “SILENT” 指定命令行是没有意义的。
.EXPORT_ALL_VARIABLES: 此目标应该作为一个简单的没有依赖的目标,它的功能是将之后的所有变量传递给子 make 进程。
.NOTPARALLEL: Makefile 中如果出现这个特殊目标,则所有的命令按照串行的方式执行,即使是存在 make 的命令行参数 “-j” 。但在递归调用的子make进程中,命令行可以并行执行。此目标不应该有依赖文件,所有出现的依赖文件将会被忽略。

② 强制目标

如果一个规则没有依赖和命令,仅有目标,那么在执行这条规则的时候,规则中的目标会被认为是每次更新的。也就是说,每当执行这条规则的时候都会认为这个目标被更新过,那么以这个目标作为依赖的那条规则中的命令,就总是会被执行。我们常用的用法是

clean:FORCE
  @rm -f *.o $(BIN)
FORCE:

这样,每次执行时, rm 命令都会被执行,相当于把 clean 定义为伪目标的效果。但是,伪目标需要 make 命令指定作为最终目标,比如 make clean 这样来指定 clean 为最终目标才可以执行。

我们做个测试,首先不加强制目标测试一下,会生成中间文件

我们在加强制目标删除中间文件

③ 空目标文件

类似于伪目标,与伪目标的区别是,空目标文件可以是一个具体的文件,但是文件的内容我们不关心,一般设置为空文件。在执行时,与伪目标一样需要 make 显示指定为最终目标。

空目标文件一般用来记录上一次执行这条规则的时间,一般本规则的实现是通过 shell 的 touch 命令实现的。在规则的命令部分,当所有命令执行完毕后,使用 touch 命令作为最后一条命令来更新目标文件的时间戳,以此实现记录命令执行时间的功能。执行 make 命令时需要指定此目标作为最终目标,如果当前目录不存在这个文件(空目标文件), touch 命令会在第一次执行时创建一个文件。

Log:1.c
    lpr -p $? # $?列出比目标文件(print)更新的所有依赖文件,并由lpr命令提交给打印机
    touch Log

一般来说,一个空目标文件应该存在一个或者多个依赖文件,将这个空目标作为最终目标,当它所依赖的文件比它更新时,此目标所在的规则的命令行将被执行。也就是说,如果空目标文件的依赖文件被改变之后,空目标文件所在的规则中定义的命令会被执行。

④ 多规则目标

如果将一个文件作为多个规则的目标,那么以这个文件为目标的规则的所有依赖文件都将会被合并为该目标文件的依赖文件列表,当这个依赖文件列表中的任何一个依赖文件比目标文件更新时,make 都会重建这个目标。虽然该目标是多个规则的目标,但是重建目标的命令只能出现在一个规则中。即使多个规则都含有重建该目标的命令,make 也只会使用最后一个规则中所定义的命令来重建,并且会提示一个错误信息。如果我们需要对相同的目标使用不同规则中定义的命令,就要使用双冒号规则来实现。

四、make 命令是如何运行的

1. make 的错误处理

(1)make 的返回值

每当 makefile 中的一条命令运行完毕,make 都会去检查命令的返回码,如果命令返回代表成功的返回码,那么 make 会继续去执行下一条命令。当一个规则中的所有命令都运行完毕且都返回成功,那么这个规则就运行成功了。如果一个规则中的某个命令出错了即命令退出码非零,那么 make 就会终止执行当前规则,并且有可能会终止所有规则的执行。

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

返回值 含义
0 执行成功
1 运行时出错
2 使用make的 -q 选项,使得一些目标不需要更新

但实际上,命令执行完毕没有返回正确(返回码非0)并不说明就一定是错误的。比如我们执行 mkdir 建立一个目录,如果目录不存在,那么执行成功返回0,如果目录存在,那么就出错并停止执行。但是,目录存在并不代表有错误,我们的目的是达到的,只要结果是有了这个目录,我们的目的就达到了,也就是说,实际上这也是正确的,我们不希望 make 停止执行。解决方法是,在命令前加一个 - (Tab 键与命令之间),这样不管命令是否出错,是否返回0,都认为运行成功。另一个方法是,给 make 加上 -i 或 --ignore-errors 参数,代表忽略命令执行中的错误。这里还有一个参数 -k 或是 --keep-going ,这个参数代表,如果某规则中的命令出错了,那么就停止该规则的执行,但继续执行其它规则。

我们在 makefile 中的特殊目标中介绍过一个特殊目标 .IGNORE ,如果一个规则是以 .IGNORE 为目标,那么这个规则中的所有命令将会忽略错误。

(2)makefile 的出错控制函数

在 makefile 中有两个函数 error 和 warning 可以控制 make 运行过程中的出错处理。当 make 执行过程中检测到某些错误时为用户提示消息,并且可以控制 make 执行过程是否继续。

① 致命错误并停止 error

  • 函数原型
$(error TEXT...)
  • 函数功能:产生致命错误,提示 TEXT… 信息给用户,并退出 make 的执行。 error 函数是在函数展也就是函数被调用的时候才提示信息并结束 make 进程。如果函数出现在命令中或者一个递归的变量定义时,读取 makefile 时不会出现错误,只有包含 error 函数引用的命令被执行,或者定义中引用此函数的递归变量被展开时,才会提示致命错误信息 TEXT… 同时退出 make。所以, error 函数一般不出现在直接展开式的变量定义中,否则在 make 读取 makefile 文件时将会提示致命错误。
  • 函数返回:无
  • 用法示例:可以看到,在示例中,会打印 error 函数执行的错误信息,并打印出错位置,且停止 make 。

② 错误提示并继续 warning

  • 函数原型
$(warning TEXT...)
  • 函数功能:用法与 error 相同,但是它不会导致致命错误(make 不会退出),仅提示 TEXT… ,且 make 继续执行。
  • 函数返回:无
  • 用法示例:对比 error 函数的示例可以看到,使用 error 函数,出错时会直接停止。而 warning 函数打印完错误信息后,make 会继续执行。

(3)常见错误

① 致命错误,通过 - 可以忽略错误,继续执行。

make 执行过程的致命错误都带有一个前缀字符串 ***

② 无法为重建目标 XXX 找到合适的规则,包括明确规则和隐含规则。

No rule to make target XXX.
No rule to make target XXX, needed by xxx.

更多的错误不再列出,make 会打印出出错位置以及错误原因,一般根据这些信息去修改即可。

2. make 命令指定文件

GNU make 搜索默认的 makefile 的规则是在当前目录下依次寻找 GNUmakefile 、 makefile 和 Makefile ,并且是按照顺序寻找这三个文件,一旦找到,就开始读取这个文件并执行,并且不会再继续搜索。

我们可以测试一下,在当前目录创建GNUmakefile 、 makefile 和 Makefile 文件,并执行 make 且不指定文件,那么 make 的默认执行的文件按优先级排序为 GNUmakefile > makefile > Makefile。

当然,我们也可以给 make 命令指定一个 makefile 文件的名字。这个功能是通过 make 的参数 -f 或 --file 或 – makefile实现的。如果在 make 的命令行一次使用多个 -f 参数,那么,所有指定的 makefile 文件将会被连在一起传递给 make 执行。

这里需要区分一下:make target 是指定 target 为最终目标(如果规则会生成这个目标的话就生成 target),而 make -f target 是指执行 target 这个 makefile 文件。指定目标和指定文件是不同的,本节所讲为通过 -f 指定文件,指定目标将在后面讲解。

3. make 命令指定目标

默认情况下,make 的最终目标是 makefile 文件中的第一个目标,其他目标都是为了生成这个最终目标而设置的中间目标。在默认情况下执行 make 命令生成的就是 makefile 中的最终目标,当然,我们也可以在执行 make 命令的时候,显示指定 make 的最终目标,直接在 make 后面接目标名(makefile 文件中存在的目标)即可。但是, - 开头或者包含 = 的目标不能被指定为 make 的最终目标,因为包含这两个字符的目标会被解析为命令行参数或是变量。即使是没有被我们明确写出来的目标也可以指定为为 make 的终极目标,只要是 make 可以找到一个隐含规则推导出该规则,那么这个隐含目标同样可以被指定成终极目标。比如下面的例子,我们在 makefile 中并没有写以 1.o 为目标的规则,甚至都没有 1.o 这个目标,但是却可以通过 make 来指定 1.o 为最终目标,这是因为 make 根据隐含规则可以推导出 1.o 的规则,并生成 1.o 这个目标。

在 make 的环境变量中有一个 MAKECMDGOALS 变量,这个环境变量中会存放我们所指定的终极目标列表,如果在命令行没有指定目标,那么这个变量就是空值。通过这个环境变量,我们可以结合条件逻辑控制来根据条件去执行命令。比如下面的例子,只要我们输入的命令不是 make clean (环境变量 MAKECMDGOALS 的值不是 clean),那么 makefile 会自动包含 1.d 和 2.d 这两个 makefile。

sources = 1.c 2.c
ifneq ( $(MAKECMDGOALS), clean)
include $(sources:.c=.d)
endif

make 命令同样可以指定最终目标为伪目标,在 makefile 发布时,常用的用于实现编译、安装、打包等功能的伪目标已经在前面 makefile 的伪目标章节列出,这些伪目标在大型工程中非常有用。

4. make 命令的参数

参数 <-n> 、 <–just-print> 、 <–dry-run> 、 <–recon>
作用 不执行命令,仅打印命令,不管目标是否更新,只是把规则和规则下的命令打印出来,但不执行。这些参数通常用于调试 makefile 来查看规则中要执行的命令。
参数 <-t>、 <–touch>
作用 把目标文件的时间更新,但不更改目标文件。也就是说,make 并不是真正的编译目标,只是把目标变成已编译过的状态。
参数 <-q> 、 <–question>
作用 搜索目标。如果目标存在,无输出且不会执行编译;如果目标不存在,打印出错信息。
参数 <-W> 、<–what-if> 、<–assume-new> 、<–new-file>
作用 后面都要加一个文件名,一般是源文件或依赖文件,Make 会根据规则推导来运行依赖于这个文件的命令,通常和 -n 参数一同使用,来查看这个依赖文件所发生的规则命令。
参数 <-B> 、<–always-make>
作用 重新编译,即认为所有目标都要更新
参数 <-f> 、<–file> 、<–makefile>
作用 后面加一个文件名,指定需要运行的 makefile 文件。
参数 <-I> 、<–include-dir>
作用 后面跟一个目录,指定一个 makefile 文件的搜索路径,可以使用多个 -I 参数来指定多个目录。

这里只是例举了几个常用的参数,更多 make 的参数,可以通过下面的命令来查看,通过 help 帮助参数,可以查看到 make 命令的所有参数。

make -h
make --help

5. makefile 文件嵌套

(1)嵌套执行 make 命令

有时候我们会对整个工程的文件进行功能划分,划分好的每个模块都有自己的 makefile 编译规则。这时,就需要用到 makefile 的嵌套执行,也就是在一个 makefile 文件中包含另一个 makefile 文件,当 make 命令执行外层 makefile 的时候会转去执行它内部包含的内层 makefile 。

首先我们建立两个目录,每个目录下都包含一个 makefile 文件

两个 makefile 文件的内容如下,在 dir2 目录下的 makefile 调用了 dir1 目录下的 makefile 文件。

我们进入到 dir2 目录并执行 make 命令

我们看到,当执行到调用嵌套 makefile 文件的语句时,会提示进入被嵌套的 makefile 文件所在的目录,并执行被嵌套的 makefile 文件,执行完毕会提示离开被嵌套的 makefile 文件所在的目录,并且继续执行外层 makefile 。

这里有一条命令

#进入 ../dir1/ 目录,并执行 make 命令
cd ../dir1/ && $(MAKE)

这句话表示,先通过 cd 命令切换到目标目录,然后在目标目录下执行 make 命令,也就是执行目标目录下的 makefile 文件。

其实,这句话也可以被替代为

$(MAKE) -C ../dir1/

在 make 中,有一个环境变量 CURDIR ,此变量代表 make 的工作目录。当使用 make 的选项 -C 时,就会进入指定的目录中去执行 make 命令,然后此变量就会被重新赋值。

一般我们把最外层的那个 makefile 文件叫做总控 makefile 。

(2)文件嵌套中的变量传递

指定变量是否传递给下一级(内嵌的)makefile 文件,使用下面两个关键字

export val    #将 val 传递给下级 makefile
unexport val  #不将 val 传递给下级 makefile

如果我们仅用一个单一的关键字 export 而不指定变量名,则表示所有变量都传递给下一级 makefile。但是有两个特殊变量 SHELL 和 MAKEFLAGS,这两个变量不管是否使用关键字 export 都会传递给下一级 makefile 文件。其中MAKEFLAGS 变量中包含了 make 命令的参数信息。如果上层 makefile 文件中定义了 MAKEFLAGS 变量,或者说在执行 make 命令的时候使用了 make 的参数,那么这些参数将会被 MAKEFLAGS 变量传递到下一层 makefile 文件,并作为 make 的参数传递。make 命令中有几个参数选项不传递,它们是 -C 、 -f 、 -o 、 -h 和 -W 选项。如果不想传递 make 的参数,可以显示的把 MAKEFLAGS 定义为空,让它传递一个空参数给下层 makefile 。

下面举例说明 export 的用法,在这个例子中,dir1/ 目录中的 makefile 使用了一个未定义的变量 Vul,这个变量来自于它的上层 makefile (包含调用它的 makefile 文件),在 dir2/ 目录下的 makefile 嵌套了dir1/ 目录中的 makefile ,并且声明了一个 export 的变量 Vul ,那么这个 Vul 变量将传递给被他嵌套的所有其他 makefile 文件。

五、实战 makefile

上面讲了很多理论知识,这一章就来实战写一个 mkefile 文件。首先准备几个文件,头文件 my_print.h 中声明了一些函数接口,这些函数接口在 my_print.c 和 print_hello.c 中实现,并在 main.c 中调用。

① 初级:使用变量、函数、模式规则

SrcFiles = $(wildcard *.c) #wildcard函数,匹配所有 .c 文件
ObjFiles = $(patsubst %.c, %.o, $(SrcFiles)) #内用替换函数及模式匹配
HeadDirs = ../header #头文件路径
#最终目标是可执行文件 exe
exe:$(ObjFiles)
    gcc $(SrcFiles) -o exe -I $(HeadDirs)
#使用模式匹配来生成中间目标
%.o:%.c
    gcc -c $< -o $@ -I $(HeadDirs)
clean:
    -@rm -f *.o
    -@rm -f exe

执行 make 命令来测试一下,这里可以明显的看到,根据模式匹配规则,每个 .c 都对应生成的 .o ,执行 make clean 可以删除目标文件。

② 中级:使用伪目标 all 来构建多个可执行文件,我们在使用的时候,只要把我们必须要执行的命令所在的规则中的目标作为 all 的依赖即可,这样就保证了这些命令一定会被执行。

1 .PHONY:all clean #伪目标,并不会真正生成相应文件
  2 SrcFiles = $(wildcard *.c) #wildcard函数,匹配所有 .c 文件
  3 ObjFiles = $(patsubst %.c, %.o, $(SrcFiles)) #内用替换函数及模式匹配
  4 HeadDirs = ../header #头文件路径
  5 
  6 #最终目标是伪目标 all,在这条规则中,all 是目标,所以 make 需要去构建它的依赖,也就是 exe1 exe2 ,这就保证了 exe1 exe2 对应规则中的命令一定会执行
  7 all:exe1 exe2
  8 
  9 exe1:$(ObjFiles)
 10     gcc $(SrcFiles) -o exe1 -I $(HeadDirs)
 11 
 12 exe2:$(ObjFiles)
 13     gcc $(SrcFiles) -o exe2 -I $(HeadDirs)
 14 
 15 #使用模式匹配来生成中间目标
 16 %.o:%.c
 17     gcc -c $< -o $@ -I $(HeadDirs)
 18 
 19 clean:
 20     -@rm -f *.o
 21     -@rm -f exe1 exe2

测试一下

需要特别强调的是。这里的伪目标 all 作为最终目标,把所有要生成的多个可执行文件作为 all 的依赖。这样,在构建最终目标 all 的时候,就需要去构建它的依赖,也就是所有的可执行文件,这样就保证了,所有可执行文件所在规则中的构建命令一定会执行,并生成这些可执行文件,而 all 作为一个伪目标,并不会真正生成。假如你不这么做,去掉伪目标 all ,直接去写 exe1 和 exe2 的规则,你会发现,它们俩只会生成一个,哪个在前面就生成哪一个,这是因为一个文件中,最终目标只有一个,make 的规则推导是以生成最终目标为目的的。

③ 高级:借助隐含规则,这里借助隐含规则自动推导 .c -> .o 的规则,不用再显示写出中间目标 .o 的规则。

1 .PHONY:all clean
  2 CC = gcc
  3 CFLAGS = -Wall -g
  4 BIN = exe
  5 HeadDirs = ../header
  6 SrcFiles = main.c print_hello.c my_print.c
  7 
  8 all:$(BIN)
  9 
 10 $(BIN):$(SrcFiles)
 11     $(CC) $(CFLAGS) $(SrcFiles) -o $(BIN) -I $(HeadDirs)
 12 
 13 clean:
 14     -@rm -f *.o $(BIN)

测试一下


总结

虽然说,在实际编写 makefile 的时候,有很多模板可以参考,并且大部分也都是使用的基本的规则命令。但是了解 makefile 的语法、变量、函数、规则等等更深层次的知识也是非常有必要的。

最后,作为 Linux 程序员或爱好者所必备的基本技能,shell 命令、VIM 编辑器、GCC 编译器、GDB 调试器、makefile 都已经讲解完毕,具体内容请查看本人 Linux 系列专栏中的文章,打好这些基本功是成为 Linux 开发高手的必备技能。

相关文章
|
22天前
|
存储 Java API
Android 浅度解析:mk预置AAR、SO文件、APP包和签名
Android 浅度解析:mk预置AAR、SO文件、APP包和签名
85 0
|
1月前
|
XML JavaScript 前端开发
xml文件使用及解析
xml文件使用及解析
|
2月前
|
存储 安全 数据管理
Linux文件时间戳:解析时区与修改时间的相互作用
Linux文件时间戳:解析时区与修改时间的相互作用
50 2
|
14天前
|
弹性计算 运维 Shell
|
15天前
|
分布式计算 Java 大数据
MaxCompute产品使用合集之大数据计算MaxCompute外部表映射了oss中的csv文件,看到"\N"被解析为"N",是什么原因
MaxCompute作为一款全面的大数据处理平台,广泛应用于各类大数据分析、数据挖掘、BI及机器学习场景。掌握其核心功能、熟练操作流程、遵循最佳实践,可以帮助用户高效、安全地管理和利用海量数据。以下是一个关于MaxCompute产品使用的合集,涵盖了其核心功能、应用场景、操作流程以及最佳实践等内容。
|
18天前
|
移动开发 数据可视化 Linux
Linux 中的文件与目录管理解析
当谈到Linux系统,文件与目录管理是其中最基本和重要的部分之一。Linux提供了一种强大而灵活的方式来组织和管理文件和目录,让用户能够轻松地访问和操作系统中的各种数据。上一节我们说到文件的属性,本文将详细介绍Linux中的文件与目录管理的各个方面。
|
18天前
|
Linux Go 数据安全/隐私保护
Linux 中的文件属性解析
在 Linux 系统中,每个文件和目录有一组属性控制其操作和访问权限。了解这些属性对有效管理文件至关重要。文件属性包括:文件类型(如 `-` 表示普通文件,`d` 表示目录),权限(如 `rwx` 表示所有者权限,`r-x` 表示组和其他用户权限),所有者,组,硬链接数,文件大小和最后修改时间。通过 `chown` 和 `chmod` 命令可更改文件所有者、所属组及权限。此外,还有特殊权限(如 SUID、SGID)和 ACL(访问控制列表)提供更精细的访问控制。
|
19天前
|
XML 人工智能 Java
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
Spring Bean名称生成规则(含源码解析、自定义Spring Bean名称方式)
|
25天前
|
XML C# 数据格式
C# 解析XML文件
C# 解析XML文件
26 1
|
28天前
|
XML 数据可视化 程序员
Qt 中的项目文件解析和命名规范
Qt 中的项目文件解析和命名规范

推荐镜像

更多