練習(xí)28:Makefile 進(jìn)階
譯者:飛龍
在下面的三個(gè)練習(xí)中你會(huì)創(chuàng)建一個(gè)項(xiàng)目的目錄框架焕妙,用于構(gòu)建之后的C程序。這個(gè)目錄框架會(huì)在這本書中剩余的章節(jié)中使用,并且這個(gè)練習(xí)中我會(huì)涉及到Makefile
便于你理解它。
這個(gè)結(jié)構(gòu)的目的是,在不憑借配置工具的情況下隧土,使構(gòu)建中等規(guī)模的程序變得容易。如果完成了它命爬,你會(huì)學(xué)到很多GNU make和一些小型shell腳本方面的東西次洼。
基本的項(xiàng)目結(jié)構(gòu)
首先要做的事情是創(chuàng)建一個(gè)C的目錄狂阿基,并且放置一些多續(xù)項(xiàng)目都擁有的遇骑,基本的文件和目錄卖毁。這是我的目錄:
$ 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
$
之后你會(huì)看到我執(zhí)行了ls -l
票堵,所以你會(huì)看到最終結(jié)果赡盘。
下面是每個(gè)文件所做的事情:
LICENSE
如果你在項(xiàng)目中發(fā)布源碼术羔,你會(huì)希望包含一份協(xié)議虽抄。如果你不這么多靡菇,雖然你有代碼的版權(quán)沃但,但是通常沒有人有權(quán)使用琢歇。
README.md
對(duì)你項(xiàng)目的簡(jiǎn)要說明谆甜。它以.md
結(jié)尾媒鼓,所以應(yīng)該作為Markdown來解析届吁。
Makefile
這個(gè)項(xiàng)目的主要構(gòu)建文件。
bin/
放置可運(yùn)行程序的地方绿鸣。這里通常是空的疚沐,Makefile會(huì)在這里生成程序。
build/
當(dāng)值庫和其它構(gòu)建組件的地方潮模。通常也是空的亮蛔,Makefile會(huì)在這里生成這些東西。
src/
放置源碼的地方擎厢,通常是.c
和.h
文件究流。
tests/
放置自動(dòng)化測(cè)試的地方。
src/dbg.h
我將練習(xí)20的dbg.h
復(fù)制到了這里动遭。
我剛才分解了這個(gè)項(xiàng)目框架的每個(gè)組件芬探,所以你應(yīng)該明白它們?cè)趺垂ぷ鳌?/p>
Makefile
我要講到的第一件事情就是Makefile,因?yàn)槟憧梢詮闹辛私馄渌鼥|西的情況厘惦。這個(gè)練習(xí)的Makeile比之前更加詳細(xì)偷仿,所以我會(huì)在你輸入它之后做詳細(xì)的分解。
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
要記住你應(yīng)該使用一致的Tab字符來縮進(jìn)Makefile。你的編輯器應(yīng)該知道怎么做炎疆,但是如果不是這樣你可以換個(gè)編輯器。沒有程序員會(huì)使用一個(gè)連如此簡(jiǎn)單的事情都做不好的編輯器国裳。
頭部
這個(gè)Makefile設(shè)計(jì)用于構(gòu)建一個(gè)庫形入,我們之后會(huì)用到它,并且通過使用GNU make
的特殊特性使它在任何平臺(tái)上都可用缝左。我會(huì)在這一節(jié)拆分它的每一部分亿遂,先從頭部開始。
Makefile:1
這是通常的CFLAGS
渺杉,幾乎每個(gè)項(xiàng)目都會(huì)設(shè)置蛇数,但是帶有用于構(gòu)建庫的其它東西。你可能需要為不同平臺(tái)調(diào)整它是越。要注意最后的OPTFLAGS
變量可以讓使用者按需擴(kuò)展構(gòu)建選項(xiàng)耳舅。
Makefile:2
用于鏈接庫的選項(xiàng),同樣也允許其它人使用OPTFLAGS
變量擴(kuò)展鏈接選項(xiàng)倚评。
Makefile:3
設(shè)置一個(gè)叫做PREFIX
的可選變量浦徊,它只在沒有PREFIX
設(shè)置的平臺(tái)上運(yùn)行Makefile時(shí)有效。這就是?=
的作用天梧。
Makefile:5
這神奇的一行通過執(zhí)行wildcard
搜索在src/
中所有*.c
文件來動(dòng)態(tài)創(chuàng)建SOURCES
變量盔性。你需要提供src/**/*.c
和src/*.c
以便GNU make能夠包含src
目錄及其子目錄的所有此類文件。
Makefile:6
一旦你創(chuàng)建了源文件列表呢岗,你可以使用patsubst
命令獲取*.c
文件的SOURCES
來創(chuàng)建目標(biāo)文件的新列表冕香。你可以告訴patsubst
把所有%.c
擴(kuò)展為%.o
,并將它們賦給OBJECTS
后豫。
Makefile:8
再次使用wildcard
來尋找所有用于單元測(cè)試的測(cè)試源文件悉尾。它們存放在不同的目錄中。
Makefile:9
之后使用相同的patsubst
技巧來動(dòng)態(tài)獲得所有TEST
目標(biāo)挫酿。其中我去掉了.c
后綴焕襟,使整個(gè)程序使用相同的名字創(chuàng)建。之前我將.c
替換為.o
來創(chuàng)建目標(biāo)文件饭豹。
Makefile:11
最后鸵赖,我將最終目標(biāo)設(shè)置為build/libYOUR_LIBRARY.a
,你可以為你實(shí)際構(gòu)建的任何庫來修改它拄衰。
這就是Makefile的頭部了它褪,但是我應(yīng)該對(duì)“讓其他人擴(kuò)展構(gòu)建”做個(gè)解釋。你在運(yùn)行它的時(shí)候可以這樣做:
# 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
中相同名稱的變量翘悉,它們會(huì)在構(gòu)建中生效茫打。你可以利用它來修改Makefile
的運(yùn)行方式。第一條命令改變了PREFIX
,使它安裝到/tmp
老赤。第二條設(shè)置了OPTFLAGS
轮洋,為之添加了pthread
選項(xiàng)。
構(gòu)建目標(biāo)
我會(huì)繼續(xù)Makefile
的分解抬旺,這一部分用于構(gòu)建目標(biāo)文件(object file)和目標(biāo)(target):
Makefile:14
要記住在沒有提供目標(biāo)時(shí)make
會(huì)默認(rèn)運(yùn)行第一個(gè)目標(biāo)弊予。這里它叫做all:
,并且它提供了$(TARGET) tests
作為構(gòu)建目標(biāo)开财。查看TARGET
變量汉柒,你會(huì)發(fā)現(xiàn)這就是庫文件,所以all:
首先會(huì)構(gòu)建出庫文件责鳍。之后碾褂,tests
目標(biāo)會(huì)構(gòu)建單元測(cè)試。
Makefile:16
另一個(gè)用于執(zhí)行“開發(fā)者構(gòu)建”的目標(biāo)历葛,它介紹了一種為單一目標(biāo)修改選項(xiàng)的技巧正塌,如果我執(zhí)行“開發(fā)構(gòu)建”,我希望CFLAGS
包含類似Wextra
這樣用于發(fā)現(xiàn)bug的選項(xiàng)恤溶。如果你將它們放到目標(biāo)的那行中传货,并再編寫一行來指向原始目標(biāo)(這里是all
),那么它就會(huì)將改為你設(shè)置的選項(xiàng)宏娄。我通常將它用于在不同的平臺(tái)上設(shè)置所需的不同選項(xiàng)问裕。
Makefile:19
構(gòu)建TARGET
庫,然而它同樣使用了15行的技巧孵坚,向一個(gè)目標(biāo)提供選項(xiàng)來為當(dāng)前目標(biāo)修改它們粮宛。這里我通過適用+=
語法為庫的構(gòu)建添加了-fPIC
。
Makefile:20
現(xiàn)在這一真實(shí)目標(biāo)首先創(chuàng)建build
目錄卖宠,之后編譯所有OBJECTS
巍杈。
Makefile:21
運(yùn)行實(shí)際創(chuàng)建TARGET
的ar
的命令。$@ $(OBJECTS)
語法的意思是扛伍,將當(dāng)前目標(biāo)的名稱放在這里筷畦,并把OBJECTS
的內(nèi)容放在后面。這里$@
的值為19行的$(TARGET)
刺洒,它實(shí)際上為build/libYOUR_LIBRARY.a
鳖宾。看起來在這一重定向中它做了很多跟蹤工作逆航,它也有這個(gè)功能鼎文,并且你可以通過修改頂部的TARGET
,來構(gòu)建一個(gè)全新的庫因俐。
Makefile:22
最后拇惋,在TARGET
上運(yùn)行ranlib
來構(gòu)建這個(gè)庫周偎。
Makefile:24-24
用于在build/
和bin/
目錄不存在的條件下創(chuàng)建它們。之后它被19行引用撑帖,那里提供了build
目標(biāo)來確保build/
目錄已創(chuàng)建蓉坎。
你現(xiàn)在擁有了用于構(gòu)建軟件的所需的所有東西。之后我們會(huì)創(chuàng)建用于構(gòu)建和運(yùn)行單元測(cè)試的東西胡嘿,來執(zhí)行自動(dòng)化測(cè)試蛉艾。
單元測(cè)試
C不同于其他語言,因?yàn)樗子跒槊總€(gè)需要測(cè)試的東西創(chuàng)建小型程序灶平。一些測(cè)試框架試圖模擬其他語言中的模塊概念伺通,并且執(zhí)行動(dòng)態(tài)加載箍土,但是它在C中并不適用逢享。這也不是必要的,因?yàn)槟憧梢詢H僅編寫一個(gè)程序用于每個(gè)測(cè)試吴藻。
我接下來會(huì)涉及到Makefile的這一部分瞒爬,并且你會(huì)看到test/
目錄中真正起作用的內(nèi)容。
Makefile:29
如果你擁有一個(gè)不是“真實(shí)”的目標(biāo)沟堡,只有有個(gè)目錄或者文件叫這個(gè)名字侧但,你需要使用g.PHONY:
標(biāo)簽來標(biāo)記它,以便make
忽略該文件航罗。
Makefile:30
我使用了與修改CFLAGS
變量相同的技巧禀横,并且將TARGET
添加到構(gòu)建中,于是每個(gè)測(cè)試程序都會(huì)鏈接TARGET
庫粥血。這里它會(huì)添加build/libYOUR_LIBRARY.a
用于鏈接柏锄。
Makefile:31
之后我創(chuàng)建了實(shí)際的test:
目錄,它依賴于所有在TESTS
變量中列出的程序复亏。這一行實(shí)際上說趾娃,“Make,請(qǐng)使用你已知的程序構(gòu)建方法缔御,以及當(dāng)前CFLAGS
設(shè)置的內(nèi)容來構(gòu)建TESTS
中的每個(gè)程序抬闷。”
Makefile:32
最后耕突,所有TESTS
構(gòu)建完之后笤成,會(huì)運(yùn)行一個(gè)我稍后創(chuàng)建的簡(jiǎn)單shell腳本,它知道如何全部運(yùn)行他們并報(bào)告它們的輸出眷茁、這一行實(shí)際上運(yùn)行它來讓你看到測(cè)試結(jié)果疹启。
Makefile:34-35
為了能夠動(dòng)態(tài)使用Valgrind
重復(fù)運(yùn)行測(cè)試,我創(chuàng)建了valgrind:
標(biāo)簽蔼卡,它設(shè)置了正確的變量并且再次運(yùn)行它喊崖。它會(huì)將Valgrind
的日志放到/tmp/valgrind-*.log
挣磨,你可以查看并了解發(fā)生了什么。之后tests/runtests.sh
看到VALGRIND
變量時(shí)荤懂,它會(huì)明白要在Valgrind
下運(yùn)行測(cè)試程序茁裙。
你需要為單元測(cè)試創(chuàng)建一個(gè)小型的shell腳本,它知道如何運(yùn)行程序节仿。我們開始創(chuàng)建這個(gè)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 ""
當(dāng)我提到單元測(cè)試如何工作時(shí)晤锥,我會(huì)在之后用到它。
清理工具
我已經(jīng)有了用于單元測(cè)試的工具廊宪,所以下一步就是創(chuàng)建需要重置時(shí)的清理工具矾瘾。
Makefile:38
clean:
目標(biāo)在我需要清理這個(gè)項(xiàng)目的任何時(shí)候都會(huì)執(zhí)行清理。
Makefile:39-42
這會(huì)清理不同編譯器和工具留下的多數(shù)垃圾箭启。它也會(huì)移除build/
目錄并且使用了一個(gè)技巧來清理XCode為調(diào)試目的而留下的*.dSYM
壕翩。
如果你碰到了想要執(zhí)行清理的垃圾,你只需要簡(jiǎn)單地?cái)U(kuò)展需要?jiǎng)h除的文件列表傅寡。
安裝
然后放妈,我會(huì)需要一種安裝項(xiàng)目的方法,對(duì)Makefile
來說就是把構(gòu)建出來的庫放到通常的PREFIX
目錄下荐操,它通常是/usr/local/lib
芜抒。
Makefile:45
它會(huì)使install:
依賴于all:
目錄,所以當(dāng)你運(yùn)行make install
之后也會(huì)先確保一切都已構(gòu)建托启。
Makefile:46
接下來我使用install
程序來創(chuàng)建lib
目標(biāo)的目錄宅倒。其中我通過使用兩個(gè)為安裝者提供便利的變量,嘗試讓安裝盡可能靈活屯耸。DESTDIR
交給安裝者拐迁,便于在安全或者特定的目錄里執(zhí)行自己的構(gòu)建。PREFIX
在別人想要將項(xiàng)目安裝到其它目錄而不是/user/local
時(shí)會(huì)被使用肩民。
Makefile:47
在此之后我使用insyall
來實(shí)際安裝這個(gè)庫唠亚,到它需要安裝的地方。
install
程序的目的是確保這些事情都設(shè)置了正確的權(quán)限持痰。當(dāng)你運(yùn)行make install
時(shí)你通常使用root權(quán)限來執(zhí)行灶搜,所以通常的構(gòu)建過程應(yīng)為make && sudo make install
。
檢查工具
Makefile
的最后一部分是個(gè)額外的部分工窍,我把它包含在我的C項(xiàng)目中用于發(fā)現(xiàn)任何使用C中“危險(xiǎn)”函數(shù)的情況割卖。這些函數(shù)是字符串函數(shù)和另一些“不保護(hù)棧”的函數(shù)患雏。
Makefile:50
設(shè)置變量鹏溯,它是個(gè)稍大的正則表達(dá)式,用于檢索類似strcpy
的危險(xiǎn)函數(shù)淹仑。
Makefile:51
這是check:
目標(biāo)丙挽,使你能夠隨時(shí)執(zhí)行檢查肺孵。
Makefile:52
它只是一個(gè)打印信息的方式,使用了@echo
來告訴make
不要打印命令颜阐,只需打印輸出平窘。
Makefile:53
對(duì)源文件運(yùn)行egrep
命令來尋找任何危險(xiǎn)的字符串。最后的|| true
是一種方法凳怨,用于防止make
認(rèn)為egrep
沒有找到任何東西是執(zhí)行失敗瑰艘。
當(dāng)你執(zhí)行它之后,它會(huì)表現(xiàn)得十分奇怪肤舞,如果沒有任何危險(xiǎn)的函數(shù)紫新,你會(huì)得到一個(gè)錯(cuò)誤。
你會(huì)看到什么
我在完成這個(gè)項(xiàng)目框架目錄的構(gòu)建之前李剖,還設(shè)置了兩個(gè)額外的練習(xí)芒率。下面這是我對(duì)Makefile
特性的測(cè)試結(jié)果:
$ 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
$
當(dāng)我運(yùn)行clean:
目標(biāo)時(shí)它會(huì)生效,但是由于我在src/
目錄中并沒有任何源文件杖爽,其它命令并沒有真正起作用敲董。我會(huì)在下個(gè)練習(xí)中補(bǔ)完它紫皇。
附加題
- 嘗試通過將源文件和頭文件添加進(jìn)
src/
慰安,來使Makefile
真正起作用,并且構(gòu)建出庫文件聪铺。在源文件中不應(yīng)該需要main
函數(shù)化焕。 - 研究
check:
目標(biāo)會(huì)使用BADFUNCS
的正則表達(dá)式來尋找什么函數(shù)。 - 如果你沒有做過自動(dòng)化測(cè)試铃剔,查詢有關(guān)資料為以后做準(zhǔn)備撒桨。