對于現代程式員來說,現在以及未來,提升開發效率比以往任何時候都更加有意義。這主要是由于不斷湧現的新技術、新工具在幫助我們解決問題的同時也将我們的時間拆分成了很多時間碎片。而變得高效的底層邏輯就是要減少時間碎片。比如寫好代碼之後不用切換到指令行運作docker build就能直接在目前視窗實作一鍵部署。而解決這個問題的辦法也很多,比如編寫一個shell或者今天要講的Makefile都是提升效率的利器。
而對于Makefile來講意義遠不止如此。比如,對c程式員來說,當所處開發環境隻有一個通過終端連接配接的Linux的時候,Makefile幾乎是建構複雜工程唯一的選擇,也是項目是否具備工程化的一個重要分水嶺。
Makefile邏輯
Makefile就是将一系列的工作流串在一起自動執行,構成Makefile最基本的要素是目标、依賴、指令。也就是為了實作目标需要哪些依賴并執行什麼樣的指令。
target: dependences1 dependences2 ...
command1 command2 ...
其中,target表示要生成的目标,dependences表示生成target需要的依賴,而command就是生成target要執行什麼指令。在格式上,指令所在行行首都有一個<tab>。
比如對于c語言來講,生成.o檔案需要.c源檔案,而生成目标二進制檔案又需要.o檔案。
test: test.o
gcc -o test test.o
test.o: test.c
gcc -c test.c -o test.o
通過上面的例子我們隐約可以感覺到Makefile的解析過程,有點類似函數的遞歸調用。總是觸及到最裡層的規則之後,後面的每一次傳回實際上都是依賴了上一次的調用。如下圖:
當然,在編寫代碼的時候target互相之間的順序有可能是打亂的,這裡不要太死闆。
Makefile的核心邏輯就是上面這點東西,而Makefile的建立有兩種方式。
第一,将檔案名指令為"Makefile",然後在Makefile檔案所在的目錄直接使用make指令就可以自動解析"Makefile"檔案的内容。比如下面是我自己的一個c語言項目的Makefile。
第二,任意命名,比如我們使用一個叫makefile_test的檔案來編寫Makefile内容。在執行make的時候使用-f參數指定檔案名。如下:
$ make -f makefile_test
當然,Makefile還支援引用其它的Makefile,格式如下:
include <filename>
僞目标
有些時候,我們希望不生成具體的目标檔案,隻想執行指令,比如在Linux通過源碼安裝經常會使用make clean來清除安裝産生的額外的中間檔案,比如:
test: test.o
gcc -o test test.o
clean:
rm -rf *.o test
按照Makefile的規則clean也是一個目标,但我們不希望生成clean目标檔案,就可以使用.PHONY将其聲明為僞目标,表示隻執行指令,不生成目标檔案。例如:
.PHONY: clean
test: test.o
gcc -o test test.o
clean:
rm -rf *.o test
當一個Makefile有多個目标的時候,可以通過參數來指定要執行哪個目标,比如上面的clean:
$ make clean
Makefile變量 Makefile也支援變量,使用上和Shell中的變量很相似,比如:
BUILDDIR=./build
...
build:
mkdir -p $(BUILDDIR)
...
上面聲明了一個變量BUILDDIR,然後在build目标中使用$(BUILDDIR)來引用變量。Makefile中變量可以分為三大類:預設變量、自定義變量和自動變量。\
C++背景開發架構師免費學習位址:C/C++Linux伺服器開發/背景架構師【零聲教育】-學習視訊教程-騰訊課堂
【文章福利】另外還整理一些C++背景開發架構師 相關學習資料,面試題,教學視訊,以及學習路線圖,免費分享有需要的可以自行點選 「連結」 群檔案共享
1. 預設變量
預設變量是Makefile的約定,比如:
test: $(CC) -o test test.c
其中CC就是一個預設變量,在linux下就是編譯器cc。其它比較常用的預設變量如下:
關于指令相關的變量
- AR : 函數庫打包程式。預設指令是 ar
- AS : 彙編語言編譯程式。預設指令是 as
- CC : C語言編譯程式。預設指令是 cc
- CXX : C++語言編譯程式。預設指令是 g++
- CO : 從 RCS檔案中擴充檔案程式。預設指令是 co
- CPP : C程式的預處理器(輸出是标準輸出裝置)。預設指令是 $(CC) –E
- FC : Fortran 和 Ratfor 的編譯器和預處理程式。預設指令是 f77
- GET : 從SCCS檔案中擴充檔案的程式。預設指令是 get
- LEX : Lex方法分析器程式(針對于C或Ratfor)。預設指令是 lex
- PC : Pascal語言編譯程式。預設指令是 pc
- YACC : Yacc文法分析器(針對于C程式)。預設指令是 yacc
- YACCR : Yacc文法分析器(針對于Ratfor程式)。預設指令是 yacc –r
- MAKEINFO : 轉換Texinfo源檔案(.texi)到Info檔案程式。預設指令是 makeinfo
- TEX : 從TeX源檔案建立TeX DVI檔案的程式。預設指令是 tex
- TEXI2DVI : 從Texinfo源檔案建立TeX DVI 檔案的程式。預設指令是 texi2dvi
- WEAVE : 轉換Web到TeX的程式。預設指令是 weave
- CWEAVE : 轉換C Web 到 TeX的程式。預設指令是 cweave
- TANGLE : 轉換Web到Pascal語言的程式。預設指令是 tangle
- CTANGLE : 轉換C Web 到 C。預設指令是 ctangle
- RM : 删除檔案指令。預設指令是 rm –f
關于指令參數的變量
- ARFLAGS : 函數庫打包程式AR指令的參數。預設值是 rv
- ASFLAGS : 彙編語言編譯器參數。(當明顯地調用 .s 或 .S 檔案時)
- CFLAGS : C語言編譯器參數。
- CXXFLAGS : C++語言編譯器參數。
- COFLAGS : RCS指令參數。
- CPPFLAGS : C預處理器參數。( C 和 Fortran 編譯器也會用到)。
- FFLAGS : Fortran語言編譯器參數。
- GFLAGS : SCCS “get”程式參數。
- LDFLAGS : 連結器參數。(如: ld )
- LFLAGS : Lex文法分析器參數。
- PFLAGS : Pascal語言編譯器參數。
- RFLAGS : Ratfor 程式的Fortran 編譯器參數。
- YFLAGS : Yacc文法分析器參數
2. 自定義變量
前面我們聲明的BUILDDIR就是一個自定義變量,要注意的是,如果聲明了一個和預設變量一樣的變量就會覆寫預設變量,這也給我們提供了一個改變預設規則的入口。
自定義變量要注意的是指派方式,在Makefile中有以下幾種指派方式:
- = 延遲指派,在Makefile運作時才會被指派
- := 立即指派,立即指派是在真正運作前就會被指派
- ?= 空指派,如果變量沒有設定過才會被指派
- += 追加指派,可以了解為字元串的加操作
延遲指派指的是在Makefile運作時再指派。比如:
test1=aa
test2=$(test1)
test1=bb
all:
echo $(test2)
上面的Makefile運作結果如下:
benggee@程式員班吉:~/app/makefile-test$ make
echo bb
bb
結果有些反直覺,最終結果是bb,而不是aa,這就是Makefile變量的延遲指派。
立即指派和我們的直覺一緻,比如上面的例子改成立即指派如下:
test1=aa
test2:=$(test1)
test1=bb
結果如下:
benggee@程式員班吉:~/app/makefile-test$ make
echo aa
aa
這就是立即指派和延遲指派的差別。
空指派,是指如果變量沒有設定的情況下才會指派,如下:
test1=aa
test1?=bb
all:
echo $(test1)
結果如下:
benggee@程式員班吉:~/app/makefile-test$ make
echo aa
aa
空指派隻會在變量沒有設定的時候才有效,這在一些場景下非常有用。比如要改一個别人的Makefile,害怕把别人的變量給覆寫掉,就可以使用?=空指派。要注意,下面的設定成空也表示變量已經設定過了,例如:
CC=
CC?=g++
上面的空指派是不會生效的,因為CC已經在前面設定過了,隻不過值是空。
追加指派,下面通過一個例子一下子就明白了,如下:
test1=aa
test1+=cc
all:
echo $(test1)
結果如下:
benggee@程式員班吉:~/app/makefile-test$ make
echo aa cc
aa cc
3. 自動變量
Makefile有很多自動變量,這裡隻介紹幾個常用的,分别是<、<、<、^、$@,其它的可以去參考Makefile文檔。
lt; 表示第一個依賴的檔案,例如:
test: test.o test2.o
echo lt;
test.o:
test2.o:
最終結果是test.o,也就是test第一個依賴。
$^ 表示所有依賴,還是上面的例子,例如:
test: test.o test2.o
echo $^
test.o:
test2.o:
最終結果是test.o test2.o,是test全部的依賴。
$@ 表示目标,上面的例子:
test: test.o test2.o
echo $@
test.o:
test2.o:
最終結果是test,也就是Makefile中的test
Makefile規則
在Makefile中有一些約定俗成的規則,正是這些規則的存在可以大大減少Makefile代碼長度,這裡我隻列出了我認為比較重要的四個規則。
1. 隐含規則
這裡以c語言的規則舉例,先來看一段Makefile:
main: main.o test.o
cc -o main main.o test.o
在目前目錄下,隻有main.c和test.c兩個檔案,并沒有.o檔案,上面的Makefile之是以能運作,是因為它的隐含規則。對于c語言來講,如果有地方依賴.o檔案,會自動去尋找相同名稱的.c檔案,并建構出.o檔案。
當然隐含規則遠沒有這麼簡單,比如Makefile還支援多個步驟的隐形規則鍊,但這裡我們隻需要了解到這一步,後面可以檢視理詳細的文檔去深入了解。
2. 通配符
Makefile中支援*、?、~三個通配符,其意義和shell中的通配符基本一緻。比如~表示宿主目錄。例如在makclean的時候清除編譯中産生的.o中間檔案,如下:
clean:
rm -rf *.o
3. 模式比對
在Makefile中模式比對使用%來實作,表示比對任意多個非空字元,相當于shell中的*。模式比對有什麼用呢?假如現在有非常多的.c源檔案要生成目标.o檔案,我們可以像下面這樣寫:
%.o: %.c
cc -c %^ -o $@
上面的意思是将所有.c檔案都經過編譯器編譯生成.o檔案,其中表示的是所有的依賴,在上面的場景中就是目前目錄下所有.c檔案。而^表示的是所有的依賴,在上面的場景中就是目前目錄下所有.c檔案。而表示的是所有的依賴,在上面的場景中就是目前目錄下所有.c檔案。而@表示目标檔案。也就是%.o所代表的所有檔案。可以看到模式比對可以大幅減少Makefile的代碼量。
4. 檔案搜尋
在比較大的工程中,程式可能會有特别多的依賴,Makefile預設會在目前目錄下搜尋依賴,但是絕大多數情況依賴可能分布在多個目錄中,Makefile的VPATH變量可以幫助我們解決依賴搜尋的問題,比如:
VPATH=src:../headers
表示Makefile會從src和..headers目錄去搜尋依賴檔案。
VPATH還支援模式比對,比如
VPATH <pattern> <directories>
比如,下面就表示在headers目錄找所有.h檔案
vpath %.h headers
還可以通過模式比對清除搜尋目錄。注意,這裡說的是清除。
VPATH <pattern>
或者清除所有已設定好的目錄。
VPATH
Makefile條件分支
Makefile條件分支比較簡單,就ifeq和ifneq。比如:
ifeq ($(ARCH), x86)
CC=gcc
else
CC=arm....gcc
endif
這個比較好了解,而ifneq的使用和ifeq幾乎是一樣的,可以自己試一下。
Makefile函數
Makefile提供了很多内置函數,但這裡我隻講其中我認為比較重要的4個函數,分别是:
1. patsubst : 模式比對與替換
patsubst的原型如下:
$(patsubst <pattern>,<replacement>,<text>)
其語義是,在text中尋找符合pattern模式的内容替換成replacement的模式。這個函數非常有用,還是以c語言為例,在沒有生成.o檔案之前我們可以通過.c格式的原檔案替換最終得到一組.o檔案名。比如:
OBJECTS=$(patsubst %.c,%.o, main.c test.c)
最終main.c和test.c會被分别替換成main.o和test.o,然後将結果指派給變量OBJECTS。
2. notdir : 去掉路徑中的目錄
notdir的原型如下:
$(notdir <text>)
有時候我們拿到的是一個檔案的全路徑,但我們隻想要檔案名,就可以使用notdir函數,比如src/foo.c,我們隻想要foo.c,就可以這樣寫:
FOO=$(notdir src/foot.c)
3. wildcard : 比對檔案
如果我們要從一堆檔案裡面挑出符合條件的那部分就可以使用wildcard,它的原型如下:
$(wildcard <pattern>)
比如我們想找出所有.h檔案
INCLUDES=$(wildcard *.h)
注意,這裡使用的通配符是"*",這裡表示在目前目錄找到所有.h檔案。
4. foreach : 批量處理
foreach可以重複相同的邏輯去處理一批資料,它的原型如下:
$(foreach <var>,<list>,<text>)
比如我們要一次性找到a、b、c三個目錄下的所有.c檔案,就可以這樣寫:
DIRS:=a b c
FILES=$(foreach dir, $(dirs), $(wildcard $(dir)/*.c))
foreach的參數有三個,我們分别來看一下
- <var> 表示從<list>中周遊出來的每一項
- <list> 是被周遊的原資料清單,可以類比c語言中的數組
- <text> 在text中是可以引用<var>的也可以使用其它函數,<text>就是foreach函數處理之後的結果,如果<text>中有函數就是函數運作之後的結果。
好了,到這裡我們所需要的前置知識都有了。下面來通過一個實際項目将上面的知識點串在一起,實作一個相對比較複雜的Makefile。
綜合實戰
接下來通過一個實戰項目練練手,x-proxy是我用c語言寫的一個基于四層的代理服務,它的目錄結構如下:
benggee@程式員班吉:~/app$ tree x-proxy/
x-proxy/
├── LICENSE
├── main.c
├── Makefile
├── proxy.conf
├── README.md
├── src
│ ├── hh.h
│ ├── log.c
│ ├── log.h
│ ├── proxy.c
│ ├── proxy.h
│ ├── route.c
│ ├── route.h
│ ├── svc.c
│ ├── svc.h
│ ├── tcpclient.c
│ ├── tcpclient.h
│ ├── tcpserver.c
│ ├── tcpserver.h
│ ├── xtime.c
│ └── xtime.h
└── test
核心代碼和相關的依賴頭檔案都放到了src目錄,在根目錄下有入口程式main.c以及Makefile檔案等。
我們希望通過Makefile自動編譯出一個xproxy二進制程式,需要實作以下的需求:
- 使用Makefile在根目錄建立一個build目錄
- 所有編譯的中間檔案,比如.o檔案都放到build目錄
- 将二進制檔案xproxy複制到根目錄
- 執行make clean删除build目錄和xproxy二進制檔案
下面我們來一步步拆解這個過程。首先,我們定義好公共的變量,如下:
# 設定編譯器
CC=gcc
# 設定要生成的目标二進制檔案
TARGET=xproxy
# 設定build目錄
BUILDDIR=build
# 設定.c檔案搜尋目錄,注意main.c在根目錄,是以根目錄也要設定進去
SRCDIR=src .
# 設定頭檔案include目錄
INCLUDEDIR=src
# 設定編譯選項,告訴編譯器頭檔案搜尋目錄
其中CFLAGS是編譯參數,最終得到的參數是-Isrc,表示從src中搜尋頭檔案,其它的沒什麼特别要說明的,接下來我們要找出所有.c源檔案,作為生成.o檔案的依賴。如下:
SOURCES=$(foreach dir, $(SRCDIR), $(windcard $(dir)/*.c))
上面表示從SRCDIR目錄找出所有.c檔案并加上目錄,比如log.c最終會被修改成src/log.c。
然後我們還要拿到所有頭檔案,代碼如下:
INCLUDES=$(foreach dir, $(INCLUDEDIR), $(wildcard $(dir)/*.h))
這裡的代碼和擷取.c檔案是一樣的邏輯。
接着我們要找到建立xproxy二進制檔案所依賴的所有.o檔案,代碼如下:
OBJECTS=$(patsubst %.c,$(BUILDDIR)/%.o, %(notdir $(SOURCES)))
我們的需求是所有編譯的中間檔案都要放在build目錄中,而$(BUILDDIR)%.o會将所有.c檔案變成build/xxx.c,這樣就相當于告訴Makefile的預設規則.o檔案是放在build中的。這裡面還用到了一個函數notdir,這是因為SOURCES中的檔案名都是帶了目錄名的全路徑名,是以要将目錄給去掉。
接着我們要告訴Makefile去哪裡找原檔案用來生成對應的.o檔案。代碼如下:
VPATH=$(SRCDIR)
可以回顧一下VPATH的作用。
現在就可以正式開始寫生成TARGET的規則了,代碼如下:
$(BUILDDIR)/$(TARGET): $(OBJECTS)
$(CC) $^ -o $@ cp -r $(BUILDDIR)/xproxy ./
注意上面的技巧,我們要生成的xproxy是要放到build目錄中的。
到這裡還差一點,上面隻是告訴make程式依賴這些OBJECTS,這個OBJECTS代表的就是build/xxx.o檔案。而這些檔案目前是還沒有的,是以我們需要生成這些檔案,代碼如下:
$(BUILDDIR)/%.o:%.c $(INCLUDES)
$(CC) $(CFLAGS) -c lt; -o $@
這裡的代碼最終會被翻譯成下面這種形式:
build/log.o xtime.o ...: log.c xtime.c... log.h xtime.h...
gcc -Isrc -c log.c xtime.c... -o $@ log.o xtime.o...
到這裡其實是有一個問題的,此時build目錄還不存在,是以需要先把build目錄建立出來。這裡有個小技巧,可以使用“|” 符号來讓兩個依賴都強制滿足,我們對上面的代碼稍作修改,如下:
$(BUILDDIR)/%.o:%.c $(INCLUDES) | build
$(CC) $(CFLAGS) -c lt; -o $@
build:
mkdir -p $(BUILDDIR)
這樣就可以在執行指令之前建立好build目錄了。
最後,我們加上make clean,如下:
clean:
rm -rf $(BUILDDIR) xproxy
對于clean和build來講并不需要生成對應的目錄檔案,是以我們可以将它們聲明為僞目錄,如下:
.PHONY: clean build
下面是最終的代碼:
CC=gcc
TARGET=xproxy
BUILDDIR=build
SRCDIR=src .
INCLUDEDIR=src
CFLAGS=$(patsubst %,-I%, $(INCLUDEDIR))
SOURCES=$(foreach dir, $(SRCDIR), $(wildcard $(dir)/*.c))
INCLUDES=$(foreach dir, $(INCLUDEDIR), $(wildcard $(dir)/*.h))
OBJECST=$(patsubst %.c, $(BUILDDIR)/%.o, $(notdir $(SOURCES)))
VPATH=$(SRCDIR)
.PHONY: clean build
$(BUILDDIR)/$(TARGET): $(OBJECTS)
$(CC) $^ -o $@ && cp -rf $(BUILDDIR)/xproxy ./
$(BUILDDIR)/%.o:%.c $(INCLUDES) | build
$(CC) $(CFLAGS) -c lt; -o $@
clean:
rm -rf $(BUILDDIR) xproxy
build:
mkdir -p $(BUILDDIR)
到這裡,就實作了一個接近生産級别的Makefile,對于這個小項目看起來似乎代碼有點多,但是它有非常好的通用性,後期我們在src目錄下新增任何.c或者.h檔案基本上都不用修改Makefile。
到這裡為止還有非常多的細節沒有提到,其實對于任何一門技術,在工作當中都不會用到所有的特性,這裡我總結了一個4/6原則,就是一門技術在實際工作中經常被用到的可能隻占了它所有内容的4成,剩下的6成很多人在職業生涯中要麼基本用不到,要麼用到的機會非常小,對于後一種情況我們隻需要了解原理,用到的時候去查文檔就可以了。
原文連結:https://juejin.cn/post/7139543023557279780