开发工具(三)

开发工具(三),第1张

内建规则

到目前为止,我们已经在makefile文件中确切的指定了如何执行过程的每一步。事实上,makefile有大量的内建规则从而可以很大程度的简化makefile文件,特别是当我们有大量源文件的时候。下面我们创建foo.c,这是一个传统的Hello World程序。

#include <stdlib.h>
#include <stdio.h>
int main()
{
    printf(“Hello World/n”);
    exit(EXIT_SUCCESS);
}

不指定makefile文件,我们尝试使用make来编译。

$ make foo
cc     foo.c -o foo
$

正如我们所看到的,make知道如何调用编译器,尽管在这种情况下,他选择cc而不是gcc(在Linux下这可以正常工作,因为通常cc链接到gcc)。有时,这些内建规则是推断规则(inference rules)。默认的规则使用宏,所以通过为这些宏指定一个新值,我们可以改变默认的行为。

$ rm foo
$ make CC=gcc CFLAGS=”-Wall -g” foo
gcc -Wall -g    foo.c   -o foo
$

我们可以使用-p选项使得make打印出其内建规则。内建规则太多而不能在这里全部列出,但是下面是GNU版本的make的make -p的简短输出,演示了其中的部分规则:

OUTPUT_OPTION = -o $@
COMPILE.c = $(CC) $(CFLAGS) $(CPPFLAGS) $(TARGET_ARCH) -c
%.o: %.c
# commands to execute (built-in):
        $(COMPILE.c) $(OUTPUT_OPTION) $<

我们现在可以通过指定构建目标文件的规则使用这些内建规则来简化我们的makefile文件,所以makefile文件的相关部分简化为:

main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h c.h

后缀与模式规则

我们所看到的内建规则使用后缀进行工作(与Windows和MS-DOS的文件名扩展相类似),所以当指定一个带有扩展名的文件时,make知道应使用哪条规则来创建带有不同扩展名的文件。在这里最通常的规则就是由以.c为结尾的文件创建以.o为结尾的文件。这个规则就是使用编译器编译文件,但是并不链接源文件。

有时我们需要能够创建新规则。程序开发作者过去在一些源文件上需要使用不同的编译器进行编译:两个在MS-DOS下,以及Linux下的gcc。要满足MS-DOS编译器的要求,C++源文件而不是C源文件,需要以.cpp为后缀进行命名。不幸的是,现在Linux下使用的make版本并没有编译.cpp文件的内建规则。(他确实具有一个在Unix下更为常见的.cc的规则)

所以或者是为每一个单独的文件指定一个规则,或者是我们需要教给make一个新的规则来由以.cpp为扩展名的文件创建目标文件。假如我们在这个工程中有大量的源文件,指定一个新规则节省了大量的输入工作,并且使得在工程中添加一个新源文件更为容易。

要添加一个新的后缀规则,我们首先在makefile文件中添加一行,告诉make新的后缀;然后我们就可以使用这个新的后缀来编写一条规则。make使用下面的语法样式来定义一条通用的规则来由具有旧后缀的文件创建具有新后缀的文件:

.<old_suffix>.<new_suffix>:

下面是我们的makefile文件中一条新的通用规则的代码片段,用于将.cpp文件转换为.o文件:

.SUFFIXES:      .cpp
.cpp.o:
   $(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c $<

特殊依赖.cpp.o:告诉make接下来的规则用于将以.cpp为后缀的文件转换为以.o为后缀的文件。当我们编写这个依赖时,我们使用特殊的宏名,因为我们并不知道我们将要转换的实际文件名。要理解这条规则,我们只需要简单的回忆起$<会扩展为起始文件名(带有旧后缀)即可。注意,我们只是告诉make如何由.cpp文件得到.o文件;make已经知道如何由一个目标文件获得二进制可执行文件。

当我们调用make时,他使用我们的新规则由bar.cpp获得bar.o,然后使用其内建规则由.o获得一个可执行文件。-xc++标记用于告诉gcc这是一个C++源文件。

在近些时候,make知道如何处理带有.cpp扩展名的C++源文件,但是当将一种文件类型转换为另一种文件类型时,这个技术是十分有用的。

更为旧的make版本包含一个对应的语法用来达到同样的效果,而且更好。例如,匹配规则使用通配符语法来匹配文件,而不是仅依赖于文件扩展名。

对于上面例子中与.cpp规则等同的模式规则如下:

%.cpp: %o
   $(CC) -xc++ $(CFLAGS) -I$(INCLUDE) -c $<

使用make管理库

当我们正处理一个大型工程时,使用库来管理多个编译产品通常是比较方便的。库是文件,通常以.a为扩展名,包含一个目标文件的集合。make命令有一个处理库的特殊语法,从而使得他们更易于管理。

这个语法就是lib (file.o),这就意味着目标文件file.o存储在库lib.a中。make具有一个内建的规则用于管理库,通常如下面的样子:

.c.a:
   $(CC) -c $(CFLAGS) $<
   $(AR) $(ARFLAGS) $@ $*.o

宏$(AR)与$(ARFLAGS)通常分别默认为命令ar与选项rv。这个简短的语法告诉make由一个.c文件得到.a库,他必须执行两条规则:

第一条规则是他必须编译源文件并且生成一个目标文件
第二条规则是使用ar命令来修改库,添加新的目标文件

所以,如果我们有一个库fud,包含文件bas.o,在第一条规则中$<被替换为bas.c。在第二条规则中,$@被替换为库fud.a,而$*被替换为bas。

试验--管理库

实际上,管理库的规则的使用是相当简单的。下面我们修改我们的程序,从而文件2.o与3.o保存在一个名为mylib.a的库中。我们的makefile文件需要一些小的修改,所以Makefile5如下所示:

all: myapp
# Which compiler
CC = gcc
# Where to install
INSTDIR = /usr/local/bin
# Where are include files kept
INCLUDE = .
# Options for development
CFLAGS = -g -Wall -ansi
# Options for release
# CFLAGS = -O -Wall -ansi
# Local Libraries
MYLIB = mylib.a
myapp: main.o $(MYLIB)
   $(CC) -o myapp main.o $(MYLIB)
$(MYLIB): $(MYLIB)(2.o) $(MYLIB)(3.o)
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h c.h
clean:
   -rm main.o 2.o 3.o $(MYLIB)
install: myapp
   @if [ -d $(INSTDIR) ]; /
    then /
      cp myapp $(INSTDIR);/
      chmod a+x $(INSTDIR)/myapp;/
      chmod og-w $(INSTDIR)/myapp;/
      echo “Installed in $(INSTDIR)”;/
   else /
      echo “Sorry, $(INSTDIR) does not exist”;/
   fi

在这里需要注意我们是如何使用默认规则来完成大多数工作的。现在让我们来测试我们的新版本makefile文件。

$ rm -f myapp *.o mylib.a
$ make -f Makefile5
gcc -g -Wall -ansi   -c -o main.o main.c
gcc -g -Wall -ansi   -c -o 2.o 2.c
ar rv mylib.a 2.o
a - 2.o
gcc -g -Wall -ansi   -c -o 3.o 3.c
ar rv mylib.a 3.o
a - 3.o
gcc -o myapp main.o mylib.a
$ touch c.h
$ make -f Makefile5
gcc -g -Wall -ansi   -c -o 3.o 3.c
ar rv mylib.a 3.o
r - 3.o
gcc -o myapp main.o mylib.a
$

工作原理

我们首先删除所有的目标文件以及库,并且允许make构建myapp,他通过编译并且在使用库链接main.o之前创建库,从而创建myapp。然后我们测试3.o的测试规则,他会通知make,如果c.h发生变动,那么3.c必须进行重新编译。他会正确的完成这些工作,在重新链接之前会编译3.c并且更新库,从而创建一个新的可执行文件myapp。

高级主题:Makefile与子目标

如果我们编写一个大工程,有时将组成库的文件由主文件分离并且存储在一个子目录中是十分方便的。使用make可以两种方法来完成这个任务。

首先,我们在此子目录可以有第二个makefile文件来编译文件,将其存储在一个库中,然后将库拷贝到上一层主目录。在高层目录中的主makefile文件然后有一条规则用于构建这个库,其调用第二个makefile文件的语法如下:

mylib.a:
   (cd mylibdirectory;$(MAKE))

这就是说我们必须总是尝试构建mylib.a。当make调用这条规则用于构建库时,他会进入子目录mylibdirectory,然后调用一个新的make命令来管理库。因为这会调用一个新的shell,使用makefile的程序并不会执行cd命令。然而,所调用的用于执行规则构建库的shell是在一个不同的目录中。括号可以保证他们都会在一个单独的shell中进行处理。

第二个方法是在一个单独的makefile文件中使用一些额外的宏。这些额外的宏是通过在我们已经讨论过的这些宏的基础上添加D(对目录而言)或是F(就文件而言)来生成的。然后我们可以用下面的规则来覆盖内建的.c.o前缀规则:

.c.o:
     $(CC) $(CFLAGS) -c $(@D)/$(<F) -o $(@D)/$(@F)

来在子目录中编译文件并且将目标文件留下子目录中。然后我们可以用如下的依赖与规则来更新当前目录中的库:

mylib.a:   mydir/2.o mydir/3.o
     ar -rv mylib.a $?

我们需要决定在我们自己的工程中我们更喜欢哪种方法。许多工程只是简单的避免具有子目录,但是这样会导致在源码目录中有大量的文件。正如我们在前面的概览中所看到的,我们在子目录中使用make只是简单的增加了复杂性。

GNU make与gcc

如果我们正使用GNU make与GNU gcc编译器,还有两个有趣的选项:

第一个就是make的-jN("jobs")选项。这会使用make同时执行N条命令。此时make可以同时调用多条规则,独立的编译工程的不同部分。依据于我们的系统配置,这对于我们重新编译的时候是一个巨大的改进。如果我们有多个源文件,尝试这个选项是很有价值的。通常而言,小的数字,例如-j3,是一个好的起点。如果我们与其他用户共享我们的机器,那么要小心使用这个选项。其他用户也许不会喜欢每次编译时我们启动大量的进程数。

另一个有用的选项就是gcc的-MM选项。这会产生一个适合于make的依赖列表。在一个具有大量源码文件的工程中,每一个文件都会包含不同的头文件组合,要正确的获得依赖关系是非常困难的,但是却是十分重要的。如果我们使用每一个源文件依赖于每一个头文件,有时我们就会编译不必须的文件。另一方面,如果我们忽略一些依赖,问题就会更为严重,因为我们会没有编译那些需要重新编译的文件。

试验--gcc -MM

下面我们使用gcc的-MM选项来为我们的例子工程生成一个依赖列表:

$ gcc -MM main.c 2.c 3.c
main.o: main.c a.h
2.o: 2.c a.h b.h
3.o: 3.c b.h c.h
$

工作原理

gcc编译只是简单的以适于插入一个makefile文件中的形式输出所需要依赖行。我们所需要做的就是将输出保存到一个临时文件中,然后将其插入makefile文件中,从而得到一个完美的依赖规则集。如果我们有一个gcc的输出拷贝,我们的依赖就没有出错的理由。

如果我们对于makefile文件十分自信,我们可以尝试使用makedepend工具,这些执行与-MM选项类似的功能,但是会将依赖实际添加到指定的makefile文件的尾部。

在我们离开makefile话题之前,也许很值得指出我们并不是只能限制自己使用makefile来编译代码或是创建库。我们可以使用他们来自动化任何任务,例如,有一个序列命令可以使得我们由一些输入文件得到一个输出文件。通常"非编译器"用户也许适用于调用awk或是sed来处理一些文件,或是生成手册页。我们可以自动化任何文件处理,只要是make由文件的日期与时间信息的修改可以处理的。

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/2083295.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-07-22
下一篇 2022-07-22

发表评论

登录后才能评论

评论列表(0条)

保存