Makefile 跟着走快點

引言  - 從"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

相關文章
相關標籤/搜索