原文:Exercise 28: Intermediate Makefileshtml
譯者:飛龍git
在下面的三個練習中你會建立一個項目的目錄框架,用於構建以後的C程序。這個目錄框架會在這本書中剩餘的章節中使用,而且這個練習中我會涉及到Makefile
便於你理解它。程序員
這個結構的目的是,在不憑藉配置工具的狀況下,使構建中等規模的程序變得容易。若是完成了它,你會學到不少GNU make和一些小型shell腳本方面的東西。github
首先要作的事情是建立一個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
,因此你會看到最終結果。shell
下面是每一個文件所作的事情:安全
LICENSE
框架
若是你在項目中發佈源碼,你會但願包含一份協議。若是你不這麼多,雖然你有代碼的版權,可是一般沒有人有權使用。編輯器
README.md
函數
對你項目的簡要說明。它以.md
結尾,因此應該做爲Markdown來解析。
Makefile
這個項目的主要構建文件。
bin/
放置可運行程序的地方。這裏一般是空的,Makefile會在這裏生成程序。
build/
當值庫和其它構建組件的地方。一般也是空的,Makefile會在這裏生成這些東西。
src/
放置源碼的地方,一般是.c
和.h
文件。
tests/
放置自動化測試的地方。
src/dbg.h
我將練習20的dbg.h
複製到了這裏。
我剛纔分解了這個項目框架的每一個組件,因此你應該明白它們怎麼工做。
我要講到的第一件事情就是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
的正則表達式來尋找什麼函數。
若是你沒有作過自動化測試,查詢有關資料爲之後作準備。