引言 - 從"HelloWorld"開始
html
Makefile 是Linux C 程序開發最重要的基本功. 表明着整個項目編譯和最終生成過程.本文重點是帶你們瞭解真實項目中那些簡易的Makefile規則構建.linux
本文參照資料git
GNU make - https://www.gnu.org/software/make/manual/make.html github
跟我一塊兒寫Makefile - http://wiki.ubuntu.org.cn/%E8%B7%9F%E6%88%91%E4%B8%80%E8%B5%B7%E5%86%99Makefile:%E6%A6%82%E8%BF%B0 shell
入門基礎Makefile概述 - https://github.com/loverszhaokai/GNUMakeManual_CNjson
推薦須要簡單看看上面資料. 特別是第三個入門教程, 瞭解基礎make語法. 看完後那咱們擴展之路開始了, 先hello world 講起. 素材 mian.cubuntu
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <time.h> #define ALEN(arr) (sizeof(arr)/sizeof(*arr)) /* * 簡單的demo測試 */ int main(int argc, char * argv[]) { int i; const char * strs[] = { "走着走着,就散了,回憶都淡了", "看着看着,就累了,星光也暗了;", "聽着聽着,就醒了,開始埋怨了;", "回頭發現,你不見了,忽然我亂了。", }; srand((unsigned)time(NULL)); for(;;) { /* * \e[ 或 \033[ 是 CSI,用來操做屏幕的。 * \e[K 表示從光標當前位置起刪除到 EOL (行尾) * \e[NX 表示將光標往X方向移動N,X = A(上) / B(下) / C(左) / D(右),\e[1A 就是把光標向上移動1行 */ printf("\033[1A\033[K"); //先回到上一行, 而後清除這一行 // 隨機輸出一段話 i = rand()%ALEN(strs); puts(strs[i]); sleep(3); } return 0; }
編譯上面程序的第一個Makefile 文件內容以下框架
main.out:main.c
gcc -g -Wall -o $@ $^
執行過程就是經過shell執行make, 咱們簡單翻譯一下上面寫法的含義.函數
目標 main.out 依賴 main.c ; main.c 已經存在(由於是存在的文件) 那就執行規則 (gcc -g -Wall -o $@ $^).工具
其中 $@ 表示全部目標, $^表示全部依賴.
是否是很簡單.固然上面Makefile還存在一些潛規則.
全部執行規則都是以\t開始; 第一個目標就是make過程惟一執行的起點;
再講以前咱們再扯一點gcc 相關的積累知識. 不然寫Makefile都是無米之炊.
# 中間插入一段關於gcc 的前戲 gcc –E –o main.i mian.c # -E是預處理展開宏,生成詳細c文件, -o是輸出 gcc –S –o main.s main.i # -S 是編譯階段, 將c文件生成彙編語言 gcc –c –o main.o main.s # -c 是彙編階段, 生成機器碼 gcc –o main.exe main.o # 連接階段, -o 生成目標執行程序 gcc –g # 編譯中加入調試信息, 方便gdb調試, 還有-ggdb3 支持宏調試等 gcc –Wall # 輸出全部警告信息 gcc –O2 # 開啓調優, O2等級調優 gcc –I(i大寫) # 導入頭文件目錄,方便 *.h文件查找 gcc –L(l 大寫) # 導入庫文件目錄,方便 *.so和*.a文件查找 gcc –l(l 小寫) # 導入庫文件, 例如-lpthread, 至關於依次查找導入 libpthread.so/libpthread.a 文件 gcc –static –l(l 小寫) # 指定只查找 靜態庫 lib*.a 文件, linux約定庫文件都是 lib開頭 ar rc libheoo.a hello.o world.o # 將*.o 文件打包成 libheoo.a 靜態庫 gcc –fPIC –shared –o libheoo.so hello.o world.o # 將*.o 文件打包成 libheoo.so 動態庫
到這裏儲備方面的講完畢了. --<-<-<@
前言 - 介紹一下實際例子中語法套路
首先升級一下上面Makefile文件, 以下(若是你複製無法執行, 請檢查規則開頭字符是\t)
# 構建全局編譯操做宏 CC = gcc CFLAGS = -g -Wall -O2 RUNE = $(CC) $(CFLAGS) -o $@ $^ RUNO = $(CC) -o $@ $< # 構建僞命令 .PHONY:all clean cleanall # 第一個標籤, 是make的開始 all:main.out main.out:main.c $(RUNE) # 清除操做 clean: -rm -rf *.i *.s *.o *~ cleanall:clean -rm -rf *.out *.out *.a *.so
咱們先說一下Makefile中變量的使用, 就是上面 "="那種基礎語法說明.
關於Makefile 變量總結以下
關於上面變量的使用這裏作一個總結. a. = 聲明變量 加入存在下面場景 … CC = cc … CC = gcc 那麼make的時候, $(CC) 就是 gcc, 會全局替換. 對於 = 聲明的能夠認爲是一個全局遞歸替換宏. b. := 聲明變量 … srcdir := ./coroutine tardir := ./Debug … 上面就是通常語言中普通變量. c. ?= 聲明變量 Foo ?= bar 上面意思是 $(foo)不存在, 那就將 bar 給它. 等同於 ifeq ($(origin FOO), undefined) FOO = bar endif d. += 聲明變量 objects = main.o foo.o bar.o utils.o objects += another.o 等同於 objects = main.o foo.o bar.o utils.o objects := $(objects) another.o
趁着熱度舉個例子, 先不解惑.
CC = cc FOO := foo BAR ?= bar HEO := heo all : echo $(CC) echo $(FOO) echo $(BAR) echo $(HEO) HEO += world FOO := FOO CC = gcc
執行結果以下, 以下圖 . 經過Demo外加上下面運行結果圖, 應該會有收穫.
經過上面咱們能夠發現 := 和 = 聲明的變量都是最終全局替換以後的結果. 他們兩者細微差異, 我仍是經過例子來講吧.
一切都在不言中, 那麼關於Makefile變量中語法講解完畢. 順帶說一些小細節吧,
1). Makefile 中 一切從簡單開始, 能用 = 就不要用 :=
2). 變量具有所有做用域 , 推薦所有用大寫命名
3). 多查最開始我推薦的資料
接着變量日後講,繼續分析其它例子
上面 .PHONY 是 Makefile中僞命令. 默認套路寫法. 定義命令名稱, 能夠經過 make 命令名稱調用.
其中 all 是Makefile第一個運行目標, 從它入口. clean , cleanall 僞命令 經過 make clean ; make cleanall 執行.
主要是清除生成的中間文件. 但願你能明白, 本身演示一下, 是否是這樣的.
這裏咱們開始一個新的例子了. 具體參照
C協程庫的編譯文件 https://github.com/wangzhione/scoroutine/blob/master/Makefile
# 全局替換變量 CC = gcc CFLAGS = -g -Wall -O2 RUNE = $(CC) $(CFLAGS) -o $@ $^ # 聲明路徑變量 SRC_PATH := ./coroutine TAR_PATH := ./Debug # 構建僞命令 .PHONY:all clean cleanall # 第一個標籤, 是make的開始 all:$(TAR_PATH)/main.out $(TAR_PATH)/main.out:main.o scoroutine.o $(CC) $(CFLAGS) -o $@ $(addprefix $(TAR_PATH)/, $^ ) $(TAR_PATH): mkdir $@ %.o:$(SRC_PATH)/%.c | $(TAR_PATH) $(CC) $(CFLAGS) -c -o $(TAR_PATH)/$@ $< # 清除操做 clean: -rm -rf $(TAR_PATH)/*.i $(TAR_PATH)/*.s $(TAR_PATH)/*.o $(TAR_PATH)/*~ cleanall:clean -rm -rf $(TAR_PATH)
從頭開始分析它的具體含義.
1) 開頭全局變量定義部分, 我的習慣問題其實也能夠用 := . 最終獲得 RUNE = gcc -g -Wall -O2 -o $@ $^ .
2) 路徑聲明部分, 用 := 聲明, 支持中間拼接. 用=也能夠, 都是條條大路同羅馬, 本身多檢查一下. 之後我可能所有用 = 聲明全局遞歸的字面變量聲明.
3) .PHONY 聲明瞭 3個僞命令. 不會當即執行的命令, 依賴 make 命令名稱 主動調用
4) all 依賴 於 $(TAR_PATH)/main.out 就是依賴於 ./coroutine/main.out. 恰好下面存在
$(TAR_PATH)/main.out:main.o scoroutine.o $(CC) $(CFLAGS) -o $@ $(addprefix $(TAR_PATH)/, $^ )
這條規則. 其中又依賴於 main.o 和 scoroutine.o 目標. 那麼兩者也會作新的目標, 就這樣遞歸的找下去.
後面找到了 %.o, Makefile中%是匹配符, 例如 main.o % 就至關於 main部分.
其中addprefix 是GNU make內置的函數的其中一個, 須要用到的時候多查文檔就好了.
爲每個能夠分割的子單元上加上一個前綴, 這個前綴就是它的第一個參數.
5) 對於下面這段很實用, 通配符 + | 生成必要文件的語法
%.o:$(SRC_PATH)/%.c | $(TAR_PATH)
$(CC) $(CFLAGS) -c -o $(TAR_PATH)/$@ $<
以上是一個通用匹配規則, %.o 目標依賴於 ..../%.c 具體文件. 後面 | 跟的也是一個依賴目標. 這個目標只會在第一次不存在的時候纔會被構建.
更加詳細的說明能夠參照第一個參照資料 "4.3 Types of Prerequisites" 部分. 這個語法用的不少, 用於構建一次生成所需的目錄信息.
6) 最後就是剩餘clean, cleanall僞命令. 定義清除中間文件等.
是否是想罵die, 可是上面那些都自行搗鼓了一遍, 基本就越過Makefile初級部分, 可以寫出能看的編譯文件O(∩_∩)O哈哈~
正文 - 來個小框架Makefile試試水
先找一個特別老的, 很水的一個Makefile 試試. 具體參照
一個控制檯小項目編譯文件 https://github.com/wangzhione/sconsole_project/blob/master/linux_sc_template/Makefile
C = gcc DEBUG = -g -Wall -D_DEBUG #指定pthread線程庫 LIB = -lpthread -lm #指定一些目錄 DIR = -I./module/schead/include -I./module/struct/include #具體運行函數 RUN = $(CC) $(DEBUG) -o $@ $^ $(LIB) $(DIR) RUNO = $(CC) $(DEBUG) -c -o $@ $^ $(DIR) # 主要生成的產品 all:test_cjson_write.out test_csjon.out test_csv.out test_json_read.out test_log.out\ test_scconf.out test_tstring.out #挨個生產的產品 test_cjson_write.out:test_cjson_write.o schead.o sclog.o tstring.o cjson.o $(RUN) test_csjon.out:test_csjon.o schead.o sclog.o tstring.o cjson.o $(RUN) test_csv.out:test_csv.o schead.o sclog.o sccsv.o tstring.o $(RUN) test_json_read.out:test_json_read.o schead.o sclog.o sccsv.o tstring.o cjson.o $(RUN) test_log.out:test_log.o schead.o sclog.o $(RUN) test_scconf.out:test_scconf.o schead.o scconf.o tree.o tstring.o sclog.o $(RUN) test_tstring.out:test_tstring.o tstring.o sclog.o schead.o $(RUN) #產品主要的待連接文件 test_cjson_write.o:./main/test_cjson_write.c $(RUNO) test_csjon.o:./main/test_csjon.c $(RUNO) test_csv.o:./main/test_csv.c $(RUNO) test_json_read.o:./main/test_json_read.c $(RUNO) test_log.o:./main/test_log.c $(RUNO) -std=c99 test_scconf.o:./main/test_scconf.c $(RUNO) test_tstring.o:./main/test_tstring.c $(RUNO) #工具集機械碼,待別人連接 schead.o:./module/schead/schead.c $(RUNO) sclog.o:./module/schead/sclog.c $(RUNO) sccsv.o:./module/schead/sccsv.c $(RUNO) tstring.o:./module/struct/tstring.c $(RUNO) cjson.o:./module/schead/cjson.c $(RUNO) scconf.o:./module/schead/scconf.c $(RUNO) tree.o:./module/struct/tree.c $(RUNO) #刪除命令 clean: rm -rf *.i *.s *.o *.out __* log ; ls -hl .PHONY:clean
上面那些註釋已經表達了一切了吧, 確實好水. 可是特別適合練手, 每個生成目標都有規則對應. 費力可是最直接. 實在沒有沒有好講的, 扯一點
1) GNU make 指定的編譯文件是 makefile 或 Makefile. 推薦用Makefile, 是一個傳統吧. 由於C項目都是小寫, 用大寫開頭以做區分.
2) Makefile 中 一樣以 \ 來起到一整行的效果
3) 其它目標, 依賴, 規則.只要存在那麼Makefile就能夠自動推導. 固然它依賴文件建立時間戳, 只有它變化了Makefile纔會從新生成目標.
Makefile點心結束了. 以上就是make使用本質, 生成什麼, 須要什麼, 執行什麼. 推薦練練手, 手冷寫不了代碼.
最後來點水果
simplec c的簡易級別框架 https://github.com/wangzhione/simplec/blob/master/Makefile
################################################################################################## # 0.前期編譯輔助參數支持 # ################################################################################################## SRC_PATH ?= ./simplec MAIN_DIR ?= main SCHEAD_DIR ?= module/schead SERVICE_DIR ?= module/service STRUCT_DIR ?= module/struct TEST_DIR ?= test TAR_PATH ?= ./Output BUILD_DIR ?= obj # 指定一些目錄 DIR = -I$(SRC_PATH)/$(SCHEAD_DIR)/include -I$(SRC_PATH)/$(SERVICE_DIR)/include \ -I$(SRC_PATH)/$(STRUCT_DIR)/include # 全局替換變量 CC = gcc LIB = -lpthread -lm CFLAGS = -g -Wall -O2 -std=gnu99 # 運行指令信息 define NOW_RUNO $(notdir $(basename $(1))).o : $(1) | $$(TAR_PATH) $$(CC) $$(CFLAGS) $$(DIR) -c -o $$(TAR_PATH)/$$(BUILD_DIR)/$$@ $$< endef # 單元測試使用, 生成指定主函數的運行程序 RUN_TEST = $(CC) $(CFLAGS) $(DIR) --entry=$(basename $@) -nostartfiles -o \ $(TAR_PATH)/$(TEST_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) # 產生具體的單元測試程序 define TEST_RUN $(1) : $$(notdir $$(basename $(1))).o libschead.a $(2) | $$(TAR_PATH) $$(RUN_TEST) $(LIB) endef ################################################################################################## # 1.具體的產品生產 # ################################################################################################## .PHONY:all clean cleanall all : main.out\ $(foreach v, $(wildcard $(SRC_PATH)/$(TEST_DIR)/*.c), $(notdir $(basename $(v))).out) # 主運行程序main main.out:main.o simplec.o libschead.a libstruct.a test_sctimeutil.o $(CC) $(CFLAGS) $(DIR) -o $(TAR_PATH)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) $(LIB) # !!!!! - 生成具體的單元測試程序 - 依賴我的維護 - !!!!! $(eval $(call TEST_RUN, test_array.out, array.o)) $(eval $(call TEST_RUN, test_atom_rwlock.out)) $(eval $(call TEST_RUN, test_cjson.out, tstr.o)) $(eval $(call TEST_RUN, test_cjson_write.out, tstr.o)) $(eval $(call TEST_RUN, test_csv.out, tstr.o)) $(eval $(call TEST_RUN, test_json_read.out, tstr.o)) $(eval $(call TEST_RUN, test_log.out)) $(eval $(call TEST_RUN, test_scconf.out, tstr.o tree.o)) $(eval $(call TEST_RUN, test_scoroutine.out, scoroutine.o)) $(eval $(call TEST_RUN, test_scpthread.out, scpthread.o scalloc.o)) $(eval $(call TEST_RUN, test_sctimer.out, sctimer.o scalloc.o)) $(eval $(call TEST_RUN, test_sctimeutil.out)) $(eval $(call TEST_RUN, test_tstring.out, tstr.o)) $(eval $(call TEST_RUN, test_xlsmtojson.out, tstr.o)) ################################################################################################## # 2.先產生所須要的全部機器碼文件 # ################################################################################################## # 循環產生 - 全部 - 連接文件 *.o SRC_CS = $(wildcard\ $(SRC_PATH)/$(MAIN_DIR)/*.c\ $(SRC_PATH)/$(TEST_DIR)/*.c\ $(SRC_PATH)/$(SCHEAD_DIR)/*.c\ $(SRC_PATH)/$(SERVICE_DIR)/*.c\ $(SRC_PATH)/$(STRUCT_DIR)/*.c\ ) $(foreach v, $(SRC_CS), $(eval $(call NOW_RUNO, $(v)))) # 生產 -相關- 靜態庫 libschead.a : $(foreach v, $(wildcard $(SRC_PATH)/$(SCHEAD_DIR)/*.c), $(notdir $(basename $(v))).o) ar cr $(TAR_PATH)/$(BUILD_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) libstruct.a : $(foreach v, $(wildcard $(SRC_PATH)/$(STRUCT_DIR)/*.c), $(notdir $(basename $(v))).o) ar cr $(TAR_PATH)/$(BUILD_DIR)/$@ $(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v)) ################################################################################################## # 3.程序的收尾工做,清除,目錄構建 # ################################################################################################## $(TAR_PATH): -mkdir -p $@/$(BUILD_DIR) -mkdir -p $@/test/config -cp -r $(SRC_PATH)/test/config $@/test # 清除操做 clean : -rm -rf $(TAR_PATH)/$(BUILD_DIR)/* cleanall : -rm -rf $(TAR_PATH)
具體能夠參照simplec 項目查看, 咱們抽一部分重點講解
define NOW_RUNO $(notdir $(basename $(1))).o : $(1) | $$(TAR_PATH) $$(CC) $$(CFLAGS) $$(DIR) -c -o $$(TAR_PATH)/$$(BUILD_DIR)/$$@ $$< endef
上面定義了一個語句塊 NOW_RUNO. 其中語句塊中除了要接收的參數能夠用$(1), $(2) ..., 其它都是兩個$$開頭, 不然就被替換了. 使用方法就是
$(eval $(call NOW_RUNO, $(v)))
經過$eval(), $(call ) 這種套路調用. call NOW_RUNO, 後面添加都是 NOW_RUNO語句塊的函數了.
這裏說一個Makefile處理的潛在小問題, 當你傳入參數是依賴項時候, 若是不是直接經過惟一一個參數傳入進去,
那麼解析的是當成多個依賴項處理.因此上面只有 $(1)作依賴項.
Makefile中 foreach語法也很好用等同於shell語法傳參方式.
$(foreach v, $^, $(TAR_PATH)/$(BUILD_DIR)/$(v))
將第二個$^經過空格分隔成單個的v代替, 被替換爲第三個中一部分. $(foreach ...)執行完畢最終返回一個拼接好的串
在簡單補充幾個函數說明 例如
$(1) => $$(notdir $$(basename $(1))).o <=> ./simplec/main/main.c => main.o
其中 nodir函數獲得文件名, basename函數獲得文件名不包過.和.後面部分.
wildcard 函數是獲得指定匹配規則下的文件全路徑拼接.
最後面 -rm 那些, 加了前綴 - 是爲了當Makefile執行到這若是運行出錯, 不中止繼續前行.
經過上面Makefile最終跑起來後, 會生成一個Output目錄, 再在內部生成 obj, test, ...
仍是頗有學習價值的. 有興趣的能夠試試.
但願經過上面講解, 可以使你之後閱讀其它更高級項目的編譯文件不那麼生疏. (* ̄(エ) ̄)
後記 - 忽然想起了什麼, 笑了笑 我本身 ...
伽羅 - http://music.163.com/#/artist/desc?id=21309