练习28:Makefile 进阶

    在下面的三个练习中你会创建一个项目的目录框架,用于构建之后的C程序。这个目录框架会在这本书中剩余的章节中使用,并且这个练习中我会涉及到便于你理解它。

    这个结构的目的是,在不凭借配置工具的情况下,使构建中等规模的程序变得容易。如果完成了它,你会学到很多GNU make和一些小型shell脚本方面的东西。

    首先要做的事情是创建一个C的目录框架,并且放置一些多续项目都拥有的,基本的文件和目录。这是我的目录:

    之后你会看到我执行了ls -l,所以你会看到最终结果。

    下面是每个文件所做的事情:

    LICENSE

    如果你在项目中发布源码,你会希望包含一份协议。如果你不这么多,虽然你有代码的版权,但是通常没有人有权使用。

    README.md

    对你项目的简要说明。它以.md结尾,所以应该作为Markdown来解析。

    Makefile

    这个项目的主要构建文件。

    bin/

    放置可运行程序的地方。这里通常是空的,Makefile会在这里生成程序。

    build/

    当值库和其它构建组件的地方。通常也是空的,Makefile会在这里生成这些东西。

    src/

    放置源码的地方,通常是.c.h文件。

    tests/

    放置自动化测试的地方。

    src/dbg.h

    我将练习20的dbg.h复制到了这里。

    我刚才分解了这个项目框架的每个组件,所以你应该明白它们怎么工作。

    Makefile

    我要讲到的第一件事情就是Makefile,因为你可以从中了解其它东西的情况。这个练习的Makeile比之前更加详细,所以我会在你输入它之后做详细的分解。

    要记住你应该使用一致的Tab字符来缩进Makefile。你的编辑器应该知道怎么做,但是如果不是这样你可以换个编辑器。没有程序员会使用一个连如此简单的事情都做不好的编辑器。

    头部

    这个Makefile设计用于构建一个库,我们之后会用到它,并且通过使用GNU make的特殊特性使它在任何平台上都可用。我会在这一节拆分它的每一部分,先从头部开始。

    Makefile:1

    这是通常的CFLAGS,几乎每个项目都会设置,但是带有用于构建库的其它东西。你可能需要为不同平台调整它。要注意最后的OPTFLAGS变量可以让使用者按需扩展构建选项。

    Makefile:2

    用于链接库的选项,同样也允许其它人使用OPTFLAGS变量扩展链接选项。

    Makefile:3

    设置一个叫做PREFIX的可选变量,它只在没有PREFIX设置的平台上运行Makefile时有效。这就是?=的作用。

    这神奇的一行通过执行wildcard搜索在src/中所有*.c文件来动态创建SOURCES变量。你需要提供src/**/*.csrc/*.c以便GNU make能够包含src目录及其子目录的所有此类文件。

    Makefile:6

    一旦你创建了源文件列表,你可以使用patsubst命令获取*.c文件的SOURCES来创建目标文件的新列表。你可以告诉patsubst把所有%.c扩展为%.o,并将它们赋给OBJECTS

    Makefile:8

    再次使用wildcard来寻找所有用于单元测试的测试源文件。它们存放在不同的目录中。

    Makefile:9

    之后使用相同的patsubst技巧来动态获得所有TEST目标。其中我去掉了.c后缀,使整个程序使用相同的名字创建。之前我将.c替换为.o来创建目标文件。

    Makefile:11

    最后,我将最终目标设置为build/libYOUR_LIBRARY.a,你可以为你实际构建的任何库来修改它。

    这就是Makefile的头部了,但是我应该对“让其他人扩展构建”做个解释。你在运行它的时候可以这样做:

    如果你传入匹配Makefile中相同名称的变量,它们会在构建中生效。你可以利用它来修改Makefile的运行方式。第一条命令改变了,使它安装到/tmp。第二条设置了OPTFLAGS,为之添加了pthread选项。

    我会继续Makefile的分解,这一部分用于构建目标文件(object file)和目标(target):

    Makefile:14

    要记住在没有提供目标时make会默认运行第一个目标。这里它叫做all:,并且它提供了$(TARGET) tests作为构建目标。查看TARGET变量,你会发现这就是库文件,所以all:首先会构建出库文件。之后,tests目标会构建单元测试。

    Makefile:16

    另一个用于执行“开发者构建”的目标,它介绍了一种为单一目标修改选项的技巧,如果我执行“开发构建”,我希望CFLAGS包含类似Wextra这样用于发现bug的选项。如果你将它们放到目标的那行中,并再编写一行来指向原始目标(这里是all),那么它就会将改为你设置的选项。我通常将它用于在不同的平台上设置所需的不同选项。

    Makefile:19

    构建TARGET库,然而它同样使用了15行的技巧,向一个目标提供选项来为当前目标修改它们。这里我通过适用+=语法为库的构建添加了-fPIC

    Makefile:20

    现在这一真实目标首先创建build目录,之后编译所有OBJECTS

    Makefile:21

    运行实际创建TARGETar的命令。$@ $(OBJECTS)语法的意思是,将当前目标的名称放在这里,并把OBJECTS的内容放在后面。这里$@的值为19行的$(TARGET),它实际上为build/libYOUR_LIBRARY.a。看起来在这一重定向中它做了很多跟踪工作,它也有这个功能,并且你可以通过修改顶部的TARGET,来构建一个全新的库。

    Makefile:22

    最后,在TARGET上运行ranlib来构建这个库。

    Makefile:24-24

    用于在build/bin/目录不存在的条件下创建它们。之后它被19行引用,那里提供了build目标来确保build/目录已创建。

    你现在拥有了用于构建软件的所需的所有东西。之后我们会创建用于构建和运行单元测试的东西,来执行自动化测试。

    单元测试

    C不同于其他语言,因为它更易于为每个需要测试的东西创建小型程序。一些测试框架试图模拟其他语言中的模块概念,并且执行动态加载,但是它在C中并不适用。这也不是必要的,因为你可以仅仅编写一个程序用于每个测试。

    我接下来会涉及到Makefile的这一部分,并且你会看到test/目录中真正起作用的内容。

    Makefile:29

    如果你拥有一个不是“真实”的目标,只有有个目录或者文件叫这个名字,你需要使用g.PHONY:标签来标记它,以便make忽略该文件。

    Makefile:30

    Makefile:31

    之后我创建了实际的test:目录,它依赖于所有在TESTS变量中列出的程序。这一行实际上说,“Make,请使用你已知的程序构建方法,以及当前CFLAGS设置的内容来构建TESTS中的每个程序。”

    Makefile:32

    最后,所有构建完之后,会运行一个我稍后创建的简单shell脚本,它知道如何全部运行他们并报告它们的输出、这一行实际上运行它来让你看到测试结果。

    Makefile:34-35

    为了能够动态使用Valgrind重复运行测试,我创建了valgrind:标签,它设置了正确的变量并且再次运行它。它会将Valgrind的日志放到/tmp/valgrind-*.log,你可以查看并了解发生了什么。之后tests/runtests.sh看到VALGRIND变量时,它会明白要在Valgrind下运行测试程序。

    你需要为单元测试创建一个小型的shell脚本,它知道如何运行程序。我们开始创建这个tests/runtests.sh脚本:

    当我提到单元测试如何工作时,我会在之后用到它。

    清理工具

    我已经有了用于单元测试的工具,所以下一步就是创建需要重置时的清理工具。

    Makefile:38

    clean:目标在我需要清理这个项目的任何时候都会执行清理。

    Makefile:39-42

    这会清理不同编译器和工具留下的多数垃圾。它也会移除build/目录并且使用了一个技巧来清理XCode为调试目的而留下的*.dSYM

    如果你碰到了想要执行清理的垃圾,你只需要简单地扩展需要删除的文件列表。

    然后,我会需要一种安装项目的方法,对Makefile来说就是把构建出来的库放到通常的PREFIX目录下,它通常是/usr/local/lib

    Makefile:45

    它会使install:依赖于all:目录,所以当你运行make install之后也会先确保一切都已构建。

    Makefile:46

    接下来我使用install程序来创建lib目标的目录。其中我通过使用两个为安装者提供便利的变量,尝试让安装尽可能灵活。DESTDIR交给安装者,便于在安全或者特定的目录里执行自己的构建。PREFIX在别人想要将项目安装到其它目录而不是/user/local时会被使用。

    Makefile:47

    在此之后我使用insyall来实际安装这个库,到它需要安装的地方。

    install程序的目的是确保这些事情都设置了正确的权限。当你运行make install时你通常使用root权限来执行,所以通常的构建过程应为make && sudo make install

    检查工具

    Makefile的最后一部分是个额外的部分,我把它包含在我的C项目中用于发现任何使用C中“危险”函数的情况。这些函数是字符串函数和另一些“不保护栈”的函数。

    Makefile:50

    设置变量,它是个稍大的正则表达式,用于检索类似strcpy的危险函数。

    Makefile:51

    这是check:目标,使你能够随时执行检查。

    Makefile:52

    它只是一个打印信息的方式,使用了来告诉make不要打印命令,只需打印输出。

    Makefile:53

    对源文件运行egrep命令来寻找任何危险的字符串。最后的|| true是一种方法,用于防止make认为egrep没有找到任何东西是执行失败。

    当你执行它之后,它会表现得十分奇怪,如果没有任何危险的函数,你会得到一个错误。

    你会看到什么

    当我运行clean:目标时它会生效,但是由于我在src/目录中并没有任何源文件,其它命令并没有真正起作用。我会在下个练习中补完它。

    • 尝试通过将源文件和头文件添加进src/,来使Makefile真正起作用,并且构建出库文件。在源文件中不应该需要main函数。
    • 如果你没有做过自动化测试,查询有关资料为以后做准备。