考察下面的示例代碼:
main.c
#include <stdio.h>
int main(){
printf("hello world!");
return 0;
}
正常情況下,通過 gcc
在命令行將其編譯后產出相應文件,可執行文件或 object 文件等。
$ gcc -o main.out main.c
上面命令編譯后運行 main.out
可執行文件。
$ ./main.out
hello world!
Make 工具
通過 make
命令,可以將上面的編譯進行有效自動化管理。通過將從輸入文件到輸出文件的編譯無則編寫成 Makefile 腳本,Make 工具將自動處理文件間依賴及是否需要編譯的檢測。
make
命令所使用的編譯配置文件可以是 Makefile
,makefile
或 GUNMake
。
其中定義任務的基本語法為:
target1 [target2 ...]: [pre-req-1 pre-req-2 ...]
[command1
command2
......]
上面形式也可稱作是一條編譯規則(rule)。
其中,
target
為任務名或文件產出。如果該任務不產出文件,則稱該任務為Phony Targets
。make
內置的 phony target 有all
,install
及clean
等,這些任務都不實際產出文件,一般用來執行一些命令。pre-req123...
這些是依賴項,即該任務所需要的外部輸入,這些輸入可以是其他文件,也可以是其他任務產出的文件。command
為該任務具體需要執行的 shell 命令。
Makefile 示例
比如文章最開始的編譯,可通過編寫下面的 Makefile 來完成:
Makefile
all:main.out
main.out: main.c
gcc -o main.out main.c
clean:
rm main.out
上面的 Makefile 中定義了三個任務,調用時可通過 make <target name>
形式來調用。
比如:
$ make main.out
gcc -o main.out main.c
產出 main.out
文件。
再比如:
$ make clean
rm main.out
該 clean
任務清除剛剛生成的 main.out
文件。
三個任務中,all
為內置的任務名,一般一個 Makefile 中都會包含,當直接調用 make
后面沒有跟任務名時,默認執行的就是 all
。
$ make
gcc -o main.out main.c
命令的換行
如果一條編譯規則中所要執行的 shell 命令有單條很長的情況,可通過 \
來換行。
main.out: main.c
gcc \
-o main.out \
main.c
注意 \
與命令結尾處需要間隔一個空格,否則識別出錯。
main.out: main.c
gcc\ # 🚨
-o main.out\ # 🚨
main.c
任務間的依賴
前面調用 all
的效果等同於調用 main.out
任務,因為 all
的輸入依賴為 main.out
文件。Make 在執行任務前會先檢查其輸入的依賴項,執行 all
時發現它依賴 main.out
文件,於是本地查找,發現本地沒有,再從 Makefile 中查找看是否有相應任務會產生該文件,結果確實有相應任務能產生該文件,所以先執行能夠產生依賴項的任務。
增量編譯
使用 Makefile 進行編譯有個好處是,在執行任務時,它會先檢查依賴項是否比需要產出的文件新,如果說依賴項更新新,則說明我們需要產出的目標文件屬於過時的產物,需要重新生成。
什么意思。比如上面的示例,當執行
$ make main.out
試圖生成 main.out
產出時,會檢查這個任務的依賴文件 main.c
是否有修改過。
比如前面我們已經執行過該任務產生過 main.out
。再次執行時,會得到如下提示:
$ make main.out
make: `main.out' is up to date.
NOTE: 上面是 Mac 上最新版本的 Make 工具(GNU Make 3.81)的提示語,老版或其他變種工具得到的可能是 Nothing to be done for \
main.out` `。
現在對輸入文件 main.c
進行修改:
#include <stdio.h>
int main(){
- printf("hello world!");
+ printf("hello wayou!");
return 0;
}
再次執行 make main.out
會發現任務正常執行並產生了新的輸出,
$ make main.out
gcc -o main.out main.c
$ ./main.out
hello wayou!⏎
這里 main.c
修改后,它在文件上來說,就比 main.out
更新了,所以我們說 main.out
這個目標, 過時(out-dated) 了。
過時的任務才會被重新執行,而未過時的會跳過,並輸出相應信息。
以上,Makefile 天然實現了增量編譯的效果,在大型項目下會節省不少編譯時間,因為它只編譯過期的任務。
Phony 類型任務的執行
需要注意的是,phony 類型的任務永遠都屬於過時類型,即,每次 make
都會執行。因為這種類型的任務它沒有文件產出,就無所謂檢查,它的主體只是調用了另外的命令而以。
拿這里的 all
來說,當我們執行 make
或 make all
時,得到:
$ make
make: Nothing to be done for `all'.
這里看不出來 all
有沒有執行,因為目前它還沒有包含任何一句命令,調用 all
后實際執行的是它的依賴文件 main.out
中的任務,而因為后者已經是最新的了,所以無須執行,所以得到了如上的輸出。
為了驗證 phony 類型任務是否每次都執行,向 all
及 main.out
中添加 echo
命令打印一些信息、
all:main.out
+ echo "[all] done"
main.out: main.c
gcc -o main.out main.c
+ echo "[main.out] done"
clean:
rm main.out
再次執行:
$ make
echo "[all] done"
[all] done
$ make
echo "[all] done"
[all] done
$ make main.out
make: `main.out' is up to date.
可以看到,屬於 phony 類型的任務 all
每次都會執行其中定義的 shell 命令,而非 phony 類型的任務 main.out
則走了增量編譯的邏輯。
變量/宏
Makefile 中可使用變量(宏)來讓腳本更加靈活和減少冗余。
其中變量使用 $
加圓括號或花括號的形式來使用,$(VAR)
,定義時類似於 C 中定義宏,所以變量也可叫 Makefile 中的宏,
CC=gcc
這里定義 CC
表示 gcc
編譯工具。然后在后續編譯命令中,就可以使用 $(CC)
代替 gcc
來書寫 shell 命令了。
+ CC=gcc
all:main.out
main.out: main.c
- gcc -o main.out main.c
+ $(CC) -o main.out main.c
clean:
rm main.out
這樣做的好處是什么?因為編譯工具可能隨着平台或環境或需要編譯的目標不同,而不同。比如 gcc
只是用來編譯 C 代碼的,如果是 C++ 你可能要用 g++
來編譯。如果是編譯 WebAssembly 則需要使用 emcc
。
無論怎樣變,我們只需要修改定義在文件開頭的 CC
變量即可,無須修改其他地方。這當然只是其中一點好處。
自動變量/Automatic Variables
自動變量/Automatic Variables 是在編譯規則匹配后工具進行設置的,具體包括:
$@
:代表產出文件名$*
:代表產出文件名不包括擴展名$<
:依賴項中第一個文件名$^
:空格分隔的去重后的所有依賴項$+
:同上,但沒去重$?
:同上,但只包含比產出更新的那些依賴
這些變量都只有一個符號,區別於正常用字母命名的變量需要使用 $(VAL)
的形式來使用,自動變量無需加括號。
利用自動變量,前面示例可改造成:
CC=gcc
TARGET=main.out
all:$(TARGET)
$(TARGET): main.c
$(CC) -o $@ $^
clean:
rm $(TARGET)
減少了重復代碼,更加易於維護,需要修改時,改動比較小。
VPATH & vpath
可通過 VPATH
指定依賴文件及產出文件的搜索目錄。
VPATH = src include
通過小寫的 vpath
可指定具體的文件名及擴展名類型,
vpath %.c src
vpath %.h include
此處 %
表示文件名。
依賴規則/Dependency Rules
Main.o : Main.h Test1.h Test2.h
Test1.o : Test1.h Test2.h
Test2.o : Test2.h
像這種,只定義了產出與依賴沒包含任務命令的無則,叫作依賴無則。因為它只定義了某個產出依賴哪些輸入,故名。
這種規則可達到這種效果,即,右邊任何文件有變更,左邊的產出便成為過時的了。
匹配規則/Pattern Rules
區別於明確指定了產出與依賴,如果一條規則包含通配符,則稱作匹配規則(Pattern Rules)。
比如,
%.o: %.c
gcc -o $@ $^
上面定義了這么一條編譯規則,將所有匹配到的 c 文件編譯成 Object 產出。
有什么用?
這種規則一般不是直接調用的,是被其他它規則觸間接使用。比如上面的依賴規則。
%.o : %.cpp
g++ -g -o $@ -c $<
Main.o : Main.h Test1.h Test2.h
Test1.o : Test1.h Test2.h
Test2.o : Test2.h
當右側這些頭文件有變動時,左邊的產出會在 make
時被檢測到過時,於是會被執行。當執行時匹配規則 %.o
會被匹配到,所以匹配規則里面的命令會執行,從而將 cpp
文件編譯成相應 Object 文件。達到了依賴更新后批量更新產出的目的,而不寫成這樣:
Main.o : Main.h Test1.h Test2.h
g++ -g -o $@ -c $<
Test1.o : Test1.h Test2.h
g++ -g -o $@ -c $<
Test2.o : Test2.h
g++ -g -o $@ -c $<
一個實際一點的示例
添加 lame 依賴到項目並將其編譯打包。
首先下載並解壓 lame 到項目目錄:
$ wget https://sourceforge.net/projects/lame/files/lame/3.100/lame-3.100.tar.gz
$ tar -zxvf lame-3.100.tar.gz
主程序中調用 lame,這只僅簡單地打印其版本信息:
main.c
#include <stdio.h>
#include "./lame-3.100/include/lame.h"
int main() {
const char* ver = get_lame_version();
printf("lame ver: %s", *ver);
return 0;
}
編譯項目的 Makefile:
Makefile
CC=gcc
TARGET=main.out
ENTRY=main.c
LAME_DIR=lame-3.100
all: $(TARGET)
$(TARGET): $(ENTRY) $(LAME_DIR)/libmp3lame/.libs/libmp3lame.a
$(CC) -o $@ </span>
$^
$(LAME_DIR)/libmp3lame/.libs/libmp3lame.a:
cd $(LAME_DIR); ./configure ; make
clean:
rm $(TARGET)
編譯並運行:
$ make
$ ./main.out
lame ver: 3⏎