# 練習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`的正則表達式來尋找什么函數。
+ 如果你沒有做過自動化測試,查詢有關資料為以后做準備。
- 笨辦法學C 中文版
- 前言
- 導言:C的笛卡爾之夢
- 練習0:準備
- 練習1:啟用編譯器
- 練習2:用Make來代替Python
- 練習3:格式化輸出
- 練習4:Valgrind 介紹
- 練習5:一個C程序的結構
- 練習6:變量類型
- 練習7:更多變量和一些算術
- 練習8:大小和數組
- 練習9:數組和字符串
- 練習10:字符串數組和循環
- 練習11:While循環和布爾表達式
- 練習12:If,Else If,Else
- 練習13:Switch語句
- 練習14:編寫并使用函數
- 練習15:指針,可怕的指針
- 練習16:結構體和指向它們的指針
- 練習17:堆和棧的內存分配
- 練習18:函數指針
- 練習19:一個簡單的對象系統
- 練習20:Zed的強大的調試宏
- 練習21:高級數據類型和控制結構
- 練習22:棧、作用域和全局
- 練習23:認識達夫設備
- 練習24:輸入輸出和文件
- 練習25:變參函數
- 練習26:編寫第一個真正的程序
- 練習27:創造性和防御性編程
- 練習28:Makefile 進階
- 練習29:庫和鏈接
- 練習30:自動化測試
- 練習31:代碼調試
- 練習32:雙向鏈表
- 練習33:鏈表算法
- 練習34:動態數組
- 練習35:排序和搜索
- 練習36:更安全的字符串
- 練習37:哈希表
- 練習38:哈希算法
- 練習39:字符串算法
- 練習40:二叉搜索樹
- 練習41:將 Cachegrind 和 Callgrind 用于性能調優
- 練習42:棧和隊列
- 練習43:一個簡單的統計引擎
- 練習44:環形緩沖區
- 練習45:一個簡單的TCP/IP客戶端
- 練習46:三叉搜索樹
- 練習47:一個快速的URL路由
- 后記:“解構 K&R C” 已死