💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
# 练习28:Makefile 进阶 > 原文:[Exercise 28: Intermediate Makefiles](http://c.learncodethehardway.org/book/ex28.html) > 译者:[飞龙](https://github.com/wizardforcel) 在下面的三个练习中你会创建一个项目的目录框架,用于构建之后的C程序。这个目录框架会在这本书中剩余的章节中使用,并且这个练习中我会涉及到`Makefile`便于你理解它。 这个结构的目的是,在不凭借配置工具的情况下,使构建中等规模的程序变得容易。如果完成了它,你会学到很多GNU make和一些小型shell脚本方面的东西。 ## 基本的项目结构 首先要做的事情是创建一个C的目录框架,并且放置一些多续项目都拥有的,基本的文件和目录。这是我的目录: ```sh $ mkdir c-skeleton $ cd c-skeleton/ $ touch LICENSE README.md Makefile $ mkdir bin src tests $ cp dbg.h src/ # this is from Ex20 $ ls -l total 8 -rw-r--r-- 1 zedshaw staff 0 Mar 31 16:38 LICENSE -rw-r--r-- 1 zedshaw staff 1168 Apr 1 17:00 Makefile -rw-r--r-- 1 zedshaw staff 0 Mar 31 16:38 README.md drwxr-xr-x 2 zedshaw staff 68 Mar 31 16:38 bin drwxr-xr-x 2 zedshaw staff 68 Apr 1 10:07 build drwxr-xr-x 3 zedshaw staff 102 Apr 3 16:28 src drwxr-xr-x 2 zedshaw staff 68 Mar 31 16:38 tests $ ls -l src total 8 -rw-r--r-- 1 zedshaw staff 982 Apr 3 16:28 dbg.h $ ``` 之后你会看到我执行了`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比之前更加详细,所以我会在你输入它之后做详细的分解。 ```makefile CFLAGS=-g -O2 -Wall -Wextra -Isrc -rdynamic -DNDEBUG $(OPTFLAGS) LIBS=-ldl $(OPTLIBS) PREFIX?=/usr/local SOURCES=$(wildcard src/**/*.c src/*.c) OBJECTS=$(patsubst %.c,%.o,$(SOURCES)) TEST_SRC=$(wildcard tests/*_tests.c) TESTS=$(patsubst %.c,%,$(TEST_SRC)) TARGET=build/libYOUR_LIBRARY.a SO_TARGET=$(patsubst %.a,%.so,$(TARGET)) # The Target Build all: $(TARGET) $(SO_TARGET) tests dev: CFLAGS=-g -Wall -Isrc -Wall -Wextra $(OPTFLAGS) dev: all $(TARGET): CFLAGS += -fPIC $(TARGET): build $(OBJECTS) ar rcs $@ $(OBJECTS) ranlib $@ $(SO_TARGET): $(TARGET) $(OBJECTS) $(CC) -shared -o $@ $(OBJECTS) build: @mkdir -p build @mkdir -p bin # The Unit Tests .PHONY: tests tests: CFLAGS += $(TARGET) tests: $(TESTS) sh ./tests/runtests.sh valgrind: VALGRIND="valgrind --log-file=/tmp/valgrind-%p.log" $(MAKE) # The Cleaner clean: rm -rf build $(OBJECTS) $(TESTS) rm -f tests/tests.log find . -name "*.gc*" -exec rm {} \; rm -rf `find . -name "*.dSYM" -print` # The Install install: all install -d $(DESTDIR)/$(PREFIX)/lib/ install $(TARGET) $(DESTDIR)/$(PREFIX)/lib/ # The Checker BADFUNCS='[^_.>a-zA-Z0-9](str(n?cpy|n?cat|xfrm|n?dup|str|pbrk|tok|_)|stpn?cpy|a?sn?printf|byte_)' check: @echo Files with potentially dangerous functions. @egrep $(BADFUNCS) $(SOURCES) || true ``` 要记住你应该使用一致的Tab字符来缩进Makefile。你的编辑器应该知道怎么做,但是如果不是这样你可以换个编辑器。没有程序员会使用一个连如此简单的事情都做不好的编辑器。 ## 头部 这个Makefile设计用于构建一个库,我们之后会用到它,并且通过使用`GNU make`的特殊特性使它在任何平台上都可用。我会在这一节拆分它的每一部分,先从头部开始。 Makefile:1 这是通常的`CFLAGS`,几乎每个项目都会设置,但是带有用于构建库的其它东西。你可能需要为不同平台调整它。要注意最后的`OPTFLAGS`变量可以让使用者按需扩展构建选项。 Makefile:2 用于链接库的选项,同样也允许其它人使用`OPTFLAGS`变量扩展链接选项。 Makefile:3 设置一个叫做`PREFIX`的可选变量,它只在没有`PREFIX`设置的平台上运行Makefile时有效。这就是`?=`的作用。 Makefile:5 这神奇的一行通过执行`wildcard`搜索在`src/`中所有`*.c`文件来动态创建`SOURCES`变量。你需要提供`src/**/*.c`和`src/*.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的头部了,但是我应该对“让其他人扩展构建”做个解释。你在运行它的时候可以这样做: ```shell # WARNING! Just a demonstration, won't really work right now. # this installs the library into /tmp $ make PREFIX=/tmp install # this tells it to add pthreads $ make OPTFLAGS=-pthread ``` 如果你传入匹配`Makefile`中相同名称的变量,它们会在构建中生效。你可以利用它来修改`Makefile`的运行方式。第一条命令改变了`PREFIX`,使它安装到`/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 运行实际创建`TARGET`的`ar`的命令。`$@ $(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 我使用了与修改`CFLAGS`变量相同的技巧,并且将`TARGET`添加到构建中,于是每个测试程序都会链接`TARGET`库。这里它会添加`build/libYOUR_LIBRARY.a`用于链接。 Makefile:31 之后我创建了实际的`test:`目录,它依赖于所有在`TESTS`变量中列出的程序。这一行实际上说,“Make,请使用你已知的程序构建方法,以及当前`CFLAGS`设置的内容来构建`TESTS`中的每个程序。” Makefile:32 最后,所有`TESTS`构建完之后,会运行一个我稍后创建的简单shell脚本,它知道如何全部运行他们并报告它们的输出、这一行实际上运行它来让你看到测试结果。 Makefile:34-35 为了能够动态使用`Valgrind`重复运行测试,我创建了`valgrind:`标签,它设置了正确的变量并且再次运行它。它会将`Valgrind`的日志放到`/tmp/valgrind-*.log`,你可以查看并了解发生了什么。之后`tests/runtests.sh`看到`VALGRIND`变量时,它会明白要在`Valgrind`下运行测试程序。 你需要为单元测试创建一个小型的shell脚本,它知道如何运行程序。我们开始创建这个`tests/runtests.sh`脚本: ```shell echo "Running unit tests:" for i in tests/*_tests do if test -f $i then if $VALGRIND ./$i 2>> tests/tests.log then echo $i PASS else echo "ERROR in test $i: here's tests/tests.log" echo "------" tail tests/tests.log exit 1 fi fi done echo "" ``` 当我提到单元测试如何工作时,我会在之后用到它。 ## 清理工具 我已经有了用于单元测试的工具,所以下一步就是创建需要重置时的清理工具。 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 它只是一个打印信息的方式,使用了`@echo`来告诉`make`不要打印命令,只需打印输出。 Makefile:53 对源文件运行`egrep`命令来寻找任何危险的字符串。最后的`|| true`是一种方法,用于防止`make`认为`egrep`没有找到任何东西是执行失败。 当你执行它之后,它会表现得十分奇怪,如果没有任何危险的函数,你会得到一个错误。 ## 你会看到什么 我在完成这个项目框架目录的构建之前,还设置了两个额外的练习。下面这是我对`Makefile`特性的测试结果: ```sh $ make clean rm -rf build rm -f tests/tests.log find . -name "*.gc*" -exec rm {} \; rm -rf `find . -name "*.dSYM" -print` $ make check Files with potentially dangerous functions. ^Cmake: *** [check] Interrupt: 2 $ make ar rcs build/libYOUR_LIBRARY.a ar: no archive members specified usage: ar -d [-TLsv] archive file ... ar -m [-TLsv] archive file ... ar -m [-abiTLsv] position archive file ... ar -p [-TLsv] archive [file ...] ar -q [-cTLsv] archive file ... ar -r [-cuTLsv] archive file ... ar -r [-abciuTLsv] position archive file ... ar -t [-TLsv] archive [file ...] ar -x [-ouTLsv] archive [file ...] make: *** [build/libYOUR_LIBRARY.a] Error 1 $ make valgrind VALGRIND="valgrind --log-file=/tmp/valgrind-%p.log" make ar rcs build/libYOUR_LIBRARY.a ar: no archive members specified usage: ar -d [-TLsv] archive file ... ar -m [-TLsv] archive file ... ar -m [-abiTLsv] position archive file ... ar -p [-TLsv] archive [file ...] ar -q [-cTLsv] archive file ... ar -r [-cuTLsv] archive file ... ar -r [-abciuTLsv] position archive file ... ar -t [-TLsv] archive [file ...] ar -x [-ouTLsv] archive [file ...] make[1]: *** [build/libYOUR_LIBRARY.a] Error 1 make: *** [valgrind] Error 2 $ ``` 当我运行`clean:`目标时它会生效,但是由于我在`src/`目录中并没有任何源文件,其它命令并没有真正起作用。我会在下个练习中补完它。 ## 附加题 + 尝试通过将源文件和头文件添加进`src/`,来使`Makefile`真正起作用,并且构建出库文件。在源文件中不应该需要`main`函数。 + 研究`check:`目标会使用`BADFUNCS`的正则表达式来寻找什么函数。 + 如果你没有做过自动化测试,查询有关资料为以后做准备。