Makefile linux 程式開發的一道坎. 也是linux開發中最重要的基本功之一, 不求深入, 但求讀懂. ...
引言 - 從"HelloWorld"開始
Makefile 是Linux C 程式開發最重要的基本功. 代表著整個項目編譯和最終生成過程.本文重點是帶大家瞭解真實項目中那些簡易的Makefile規則構建.
本文參照資料
GNU make - https://www.gnu.org/software/make/manual/make.html
跟我一起寫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
入門基礎Makefile概述 - https://github.com/loverszhaokai/GNUMakeManual_CN
推薦需要簡單看看上面資料. 特別是第三個入門教程, 瞭解基礎make語法. 看完後那我們擴展之路開始了, 先hello world 講起. 素材 mian.c
#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