考察下面的示例代碼:
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⏎
