Makefile文件教程(二)

八、Makefile路径搜索使用案例

我们了解了一下路径搜索的使用方式,我们再来看一下具体的使用方法。

为了体验实例的效果的更加明显,我们按照源代码树的布局来放置文件。我们把源代码放置在src目录下,包含的文件文件是:list1.c、list2.c、main.c 文件,我们把头文件包含在 include 的目录下,包含文件 list1.h、list2.h 文件。Makefile 放在这两个目录文件的上一级目录。

我们按照之前的方式来编写 Makefile 文件:

  1. main:main.o list1.o list2.o
  2. gcc -o $@ $<
  3. main.o:main.c
  4. gcc -o $@ $^
  5. list1.o:list1.c list1.h
  6. gcc -o $@ $<
  7. list2.o:list2.c list2.h
  8. gcc -o $@ $<

我们编译执行的 make 时候会发现命令行提示我们:

make:*** No rule to make target 'main.c',need by 'main.o'. stop.

出现错误并且编译停止了,为什么会出现错误呢?我们来看一下出现错误的原因,再去重建最终目标文件 main 的时候我们需要 main.o 文件,但是我们再去重建目标main.o 文件的时候,发现没有找到指定的 main.c 文件,这是错误的根本原因。

这个时候我们就应该添加上路径搜索,我们知道路径搜索的方法有两个:VPATH 和 vpath。我们先来使用一下 VPATH,使用方式很简单,我们只需要在上述的文件开头加上这样一句话:

VPATH=src include

再去执行 make 就不会出现错误。所以 Makefile 中的最终写法是这样的:

VPATH=src include
main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<

我们使用 vpath 的话同样可以解决这样的问题,只需要把上述代码中的 VPATH 所在行的代码改写成:

vpath %.c src
vpath %.h include

这样我们就可以用 vpath 实现功能,代码的最终展示为:

vpath %.c src
vpath %.h include
main:main.o list1.o list2.o
    gcc -o $@ $<
main.o:main.c
    gcc -o $@ $^
list1.o:list1.c list1.h
    gcc -o $@ $<
list2.o:list2.c list2.h
    gcc -o $@ $<

九、Makefile隐含规则

这个章节讲述的是 Makefile 的隐含规则,所谓的隐含规则就是需要我们做出具体的操作,系统自动完成。编写 Makefile 的时候,可以使用隐含规则来简化Makefile 文件编写。

实例:

  1. test:test.o
  2. gcc -o test test.o
  3. test.o:test.c

我们可以在 Makefile 中这样写来编译 test.c 源文件,相比较之前少写了重建 test.o 的命令。但是执行 make,发现依然重建了 test 和 test.o 文件,运行结果却没有改变。这其实就是隐含规则的作用。在某些时候其实不需要给出重建目标文件的命令,有的甚至可以不需要给出规则。实例:

  1. test:test.o
  2. gcc -o test test.o

运行的结果是相同的。

注意:隐含条件只能省略中间目标文件重建的命令和规则,但是最终目标的命令和规则不能省略。

隐含规则的具体的工作流程:make 执行过程中找到的隐含规则,提供了此目标的基本依赖关系。确定目标的依赖文件和重建目标需要使用的命令行。隐含规则所提供的依赖文件只是一个基本的(在C语言中,通常他们之间的对应关系是:test.o 对应的是 test.c 文件)。当需要增加这个文件的依赖文件的时候要在 Makefile 中使用没有命令行的规则给出。实例:

  1. test:test.o
  2. gcc -o test test.o
  3. test:test1.h

其实在有些时候隐含规则的使用会出现问题。因为有一个 make 的“隐含规则库”。库中的每一条隐含规则都有相应的优先级顺序,优先级也就会越高,使用时也就会被优先使用。

例如在 Makefile 中添加这行代码:

foo.o:foo.p

我们都知道 .p 文件是 Pascal 程序的源文件,如果书写规则时不加入命令的话,那么 make 会按照隐含的规则来重建目标文件 foo.o。如果当前目录下恰好存在 foo.c 文件的时候,隐含规则会把 foo.c 当做是 foo.o 的依赖文件进行目标文件的重建。因为编译 .c 文件的隐含规则在编译 .p 文件之前,显然优先级也会越高。当 make 找到生成 foo.o 的文件之后,就不会再去寻找下一条规则。如果我们不想使用隐含规则,在使用的时候不仅要声明规则,也要添加上执行的命令。

这里讲的是预先设置的隐含规则。如果不明确的写下规则,那么make 就会自己寻找所需要的规则和命令。当然我们也可以使用 make 选项:-r-n-builtin-rules选项来取消所有的预设值的隐含规则。当然即使是指定了“-r”的参数,某些隐含规则还是会生效。因为有很多的隐含规则都是使用了后缀名的规则来定义的,所以只要隐含规则中含有“后缀列表”那么隐含规则就会生效。默认的列表是:

.out、.a、.in、.o、.c、.cc、.C、.p、.f、.F、.r、.y、.l、.s、.S、.mod、.sym、.def、
.h、.info、.dvi、.tex、.texinfo、.texi、.txinfo、.w、.ch、.web、.sh、.elc、.el。

下面是一些常用的隐含规则:

  • 编译 C 程序
  • 编译 C++ 程序
  • 编译 Pascal 程序
  • 编译 Fortran/Ratfor 程序
  • 预处理 Fortran/Ratfor 程序
  • 编译 Modula-2 程序
  • 汇编和需要预处理的汇编程序
  • 链接单一的 object 文件
  • Yacc C 程序
  • Lex C 程序时的隐含规则


上面的编译顺序都是一些常用的编程语言执行隐含规则的顺序,我们在 Makefile 中指定规则时,可以参考这样的列表。当需要编译源文件的时候,考虑是不是需要使用隐含规则。如果不需要,就要把相应的规则和命令全部书写上去。

内嵌隐含规则的命令中,所使用的变量都是预定义的。我们将这些变量称为“隐含变量”。这些变量允许修改:可以通过命令行参数传递或者是设置系统环境变量的方式都可以对它进行重新定义。无论使用哪种方式,只要 make 在运行的,这些变量的定义有效。Makefile 的隐含规则都会使用到这些变量。

比如我们编译 .c 文件在我们的 Makefile 中就是隐含的规则,默认使用到的编译命令时cc,执行的命令时cc -c我们可以对用上面的任何一种方式将CC定义为ncc。这样我们就编译 .c 文件的时候就可以用ncc进行编译。

隐含规则中使用的变量可以分成两类:
1.代表一个程序的名字。例如:“CC”代表了编译器的这个可执行程序。
2.代表执行这个程序使用的参数.例如:变量“CFLAGS”。多个参数之间使用空格隔开。

下面我们来列举一下代表命令的变量,默认都是小写。

  • AR:函数库打包程序,科创价静态库 .a 文档。
  • AS:应用于汇编程序。
  • CC:C 编译程序。
  • CXX:C++编译程序。
  • CO:从 RCS 中提取文件的程序。
  • CPP:C程序的预处理器。
  • FC:编译器和与处理函数 Fortran 源文件的编译器。
  • GET:从CSSC 中提取文件程序。
  • LEX:将Lex语言转变为 C 或 Ratfo 的程序。
  • PC:Pascal 语言编译器。
  • YACC:Yacc 文法分析器(针对于C语言)
  • YACCR:Yacc 文法分析器。

十、Makefile ifeq、ifneq、ifdef和ifndef(条件判断)

日常使用 Makefile 编译文件时,可能会遇到需要分条件执行的情况,比如在一个工程文件中,可编译的源文件很多,但是它们的类型是不相同的,所以编译文件使用的编译器也是不同的。手动编译去操作文件显然是不可行的(每个文件编译时需要注意的事项很多),所以 make 为我们提供了条件判断来解决这样的问题。

需要解决的问题:要根据判断,分条件执行语句。
条件语句的作用:条件语句可以根据一个变量的值来控制 make 执行或者时忽略 Makefile 的特定部分,条件语句可以是两个不同的变量或者是常量和变量之间的比较。

条件语句使用优点:Makefile 中使用条件控制可以做到处理的灵活性和高效性。

注意:条件语句只能用于控制 make 实际执行的 Makefile 文件部分,不能控制规则的 shell 命令执行的过程。

下面是条件判断中使用到的一些关键字:
 

关键字 功能
ifeq 判断参数是否不相等,相等为 true,不相等为 false。
ifneq 判断参数是否不相等,不相等为 true,相等为 false。
ifdef 判断是否有值,有值为 true,没有值为 false。
ifndef 判断是否有值,没有值为 true,有值为 false。

ifeq 和 ifneq

条件判断的使用方式如下:

ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

实例:

  1. libs_for_gcc= -lgnu
  2. normal_libs=
  3. foo:$(objects)
  4. ifeq($(CC),gcc)
  5. $(CC) -o foo $(objects) $(libs_for_gcc)
  6. else
  7. $(CC) -o foo $(objects) $(noemal_libs)
  8. endif

条件语句中使用到三个关键字“ifeq”、“else”、“endif”。其中:“ifeq”表示条件语句的开始,并指定一个比较条件(相等)。括号和关键字之间要使用空格分隔,两个参数之间要使用逗号分隔。参数中的变量引用在进行变量值比较的时候被展开。“ifeq”,后面的是条件满足的时候执行的,条件不满足忽略;“else”表示当条件不满足的时候执行的部分,不是所有的条件语句都要执行此部分;“endif”是判断语句结束标志,Makefile 中条件判断的结束都要有。

其实 "ifneq" 和 "ifeq" 的使用方法是完全相同的,只不过是满足条件后执行的语句正好相反。

上面的例子可以换一种更加简介的方式来写:

  1. libs_for_gcc= -lgnu
  2. normal_libs=
  3. ifeq($(CC),gcc)
  4. libs=$(libs_for_gcc)
  5. else
  6. libs=$(normal_libs)
  7. endif
  8. foo:$(objects)
  9. $(CC) -o foo $(objects) $(libs)

ifdef 和 ifndef 

使用方式如下:

ifdef VARIABLE-NAME

它的主要功能是判断变量的值是不是为空,实例:

实例 1:

  1. bar =
  2. foo = $(bar)
  3. all:
  4. ifdef foo
  5. @echo yes
  6. else
  7. @echo no
  8. endif

实例 2:

  1. foo=
  2. all:
  3. ifdef foo
  4. @echo yes
  5. else
  6. @echo no
  7. endif

通过两个实例对比说明:通过打印 "yes" 或 "no" 来演示执行的结果。我们执行 make 可以看到实例 1打印的结果是 "yes" ,实例 2打印的结果是 "no" 。其原因就是在实例 1 中,变量“foo”的定义是“foo = $(bar)”。虽然变量“bar”的值为空,但是“ifdef”的判断结果为真,这种方式判断显然是有不行的,因此当我们需要判断一个变量的值是否为空的时候需要使用“ifeq" 而不是“ifdef”。

注意:在 make 读取 Makefile 文件时计算表达式的值,并根据表达式的值决定判断语句中的哪一个部分作为此 Makefile 所要执行的内容。因此在条件表达式中不能使用自动化变量,自动化变量在规则命令执行时才有效,更不能将一个完整的条件判断语句分卸在两个不同的 Makefile 的文件中。在一个 Makefile 中使用指示符 "include" 包含另一个 Makefile 文件。

十一、Makefile伪目标

这一个章节我们主要讲的是 Makefile 中的伪目标。所谓的伪目标可以这样来理解,它并不会创建目标文件,只是想去执行这个目标下面的命令。伪目标的存在可以帮助我们找到命令并执行。

使用伪目标有两点原因:

  • 避免我们的 Makefile 中定义的只执行的命令的目标和工作目录下的实际文件出现名字冲突。
  • 提高执行 make 时的效率,特别是对于一个大型的工程来说,提高编译的效率也是我们所必需的。


我们先来看一下第一种情况的使用。如果需要书写这样一个规则,规则所定义的命令不是去创建文件,而是通过 make 命令明确指定它来执行一些特定的命令。实例:

  1. clean:
  2. rm -rf *.o test

规则中 rm 命令不是创建文件 clean 的命令,而是执行删除任务,删除当前目录下的所有的 .o 结尾和文件名为 test 的文件。当工作目录下不存在以 clean 命令的文件时,在 shell 中输入 make clean 命令,命令 rm -rf *.o test 总会被执行 ,这也是我们期望的结果。
 
如果当前目录下存在文件名为  clean 的文件时情况就会不一样了,当我们在 shell 中执行命令 make clean,由于这个规则没有依赖文件,所以目标被认为是最新的而不去执行规则所定义的命令。因此命令 rm 将不会被执行。为了解决这个问题,删除 clean 文件或者是在 Makefile 中将目标 clean 声明为伪目标。将一个目标声明称伪目标的方法是将它作为特殊的目标.PHONY的依赖,如下:

.PHONY:clean

这样 clean 就被声明成一个伪目标,无论当前目录下是否存在 clean 这个文件,当我们执行 make clean 后 rm 都会被执行。而且当一个目标被声明为伪目标之后,make 在执行此规则时不会去试图去查找隐含的关系去创建它。这样同样提高了 make 的执行效率,同时也不用担心目标和文件名重名而使我们的编译失败。

在书写伪目标的时候,需要声明目标是一个伪目标,之后才是伪目标的规则定义。目标 "clean" 的完整书写格式如下:

  1. .PHONY:clean
  2. clean:
  3. rm -rf *.o test

伪目标的另一种使用的场合是在 make 的并行和递归执行的过程中,此情况下一般会存在一个变量,定义为所有需要 make 的子目录。对多个目录进行 make 的实现,可以在一个规则的命令行中使用 shell 循环来完成。如下:

  1. SUBDIRS=foo bar baz
  2. subdirs:
  3. for dir in $(SUBDIRS);do $(MAKE) -C $$dir;done

代码表达的意思是当前目录下存在三个子文件目录,每个子目录文件都有相对应的 Makefile 文件,代码中实现的部分是用当前目录下的 Makefile 控制其它子模块中的 Makefile 的运行,但是这种实现方法存在以下几个问题:

  •  当子目录执行 make 出现错误时,make 不会退出。就是说,在对某个目录执行 make 失败以后,会继续对其他的目录进行 make。在最终执行失败的情况下,我们很难根据错误提示定位出具体实在那个目录下执行 make 发生的错误。这样给问题定位造成很大的困难。为了解决问题可以在命令部分加入错误检测,在命令执行的错误后主动退出。不幸的是如果在执行 make 时使用了 "-k" 选项,此方式将失效。
  • 另外一个问题就是使用这种 shell 循环方式时,没有用到 make 对目录的并行处理功能由于规则的命令时一条完整的 shell 命令,不能被并行处理。

有了伪目标之后,我们可以用它来克服以上方式所存在的两个问题,代码展示如下:

  1. SUBDIRS=foo bar baz
  2. .PHONY:subdirs $(SUBDIRS)
  3. subdirs:$(SUBDIRS)
  4. $(SUBDIRS):
  5. $(MAKE) -C $@
  6. foo:baz

上面的实例中有一个没有命令行的规则“foo:baz”,这个规则是用来规定三个子目录的编译顺序。因为在规则中 "baz" 的子目录被当作成了 "foo" 的依赖文件,所以 "baz" 要比 "foo" 子目录更先执行,最后执行 "bar" 子目录的编译。

一般情况下,一个伪目标不作为另外一个目标的依赖。这是因为当一个目标文件的依赖包含伪目标时,每一次在执行这个规则伪目标所定义的命令都会被执行(因为它作为规则的依赖,重建规则目标时需要首先重建规则的所有依赖文件)。当一个伪目标没有任何目标(此目标是一个可被创建或者是已存在的文件)的依赖时,我们只能通过 make 的命令来明确的指定它的终极目标,执行它所在规则所定义的命令。例如 make clean。

 伪目标实现多文件编辑

如果在一个文件里想要同时生成多个可执行文件,我们可以借助伪目标来实现。使用方式如下:

  1. .PHONY:all
  2. all:test1 test2 test3
  3. test1:test1.o
  4. gcc -o $@ $^
  5. test2:test2.o
  6. gcc -o $@ $^
  7. test3:test3.o
  8. gcc -o $@ $^

我们在当前目录下创建了三个源文件,目的是把这三个源文件编译成为三个可执行文件。将重建的规则放到 Makefile 中,约定使用 "all" 的伪目标来作为最终目标,它的依赖文件就是要生成的可执行文件。这样的话只需要一个 make 命令,就会同时生成三个可执行文件。
之所以这样写,是因为伪目标的特性,它总会被执行,所以它依赖的三个文件的目标就不如 "all" 这个目标新,所以,其他的三个目标的规则总是被执行,这也就达到了我们一口气生成多个目标的目的。我们也可以实现单独的编译这三个中的任意一个源文件(我们想去重建 test1,我们可以执行命令make test1 来实现 )。 

猜你喜欢

转载自blog.csdn.net/qq_43590614/article/details/114330523