練習28:Makefile 進階
原文:Exercise 28: Intermediate Makefiles
譯者:飛龍
在下面的三個練習中你會建立一個專案的目錄框架,用於構建之後的C程式。這個目錄框架會在這本書中剩餘的章節中使用,並且這個練習中我會涉及到Makefile
便於你理解它。
這個結構的目的是,在不憑藉配置工具的情況下,使構建中等規模的程式變得容易。如果完成了它,你會學到很多GNU make和一些小型shell指令碼方面的東西。
基本的專案結構
首先要做的事情是建立一個C的目錄框架,並且放置一些多續專案都擁有的,基本的檔案和目錄。這是我的目錄:
$ 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比之前更加詳細,所以我會在你輸入它之後做詳細的分解。
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的頭部了,但是我應該對“讓其他人擴充套件構建”做個解釋。你在執行它的時候可以這樣做:
# 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
指令碼:
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
特性的測試結果:
$ 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
的正規表示式來尋找什麼函式。 -
如果你沒有做過自動化測試,查詢有關資料為以後做準備。