Make 編譯腳本上手


考察下面的示例代碼:

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 命令所使用的編譯配置文件可以是 MakefilemakefileGUNMake

其中定義任務的基本語法為:

target1 [target2 ...]: [pre-req-1 pre-req-2 ...]
	[command1
	 command2
	 ......]

上面形式也可稱作是一條編譯規則(rule)。

其中,

  • target 為任務名或文件產出。如果該任務不產出文件,則稱該任務為 Phony Targetsmake 內置的 phony target 有 all, installclean 等,這些任務都不實際產出文件,一般用來執行一些命令。
  • 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 來說,當我們執行 makemake all 時,得到:

$ make
make: Nothing to be done for `all'.

這里看不出來 all 有沒有執行,因為目前它還沒有包含任何一句命令,調用 all 后實際執行的是它的依賴文件 main.out 中的任務,而因為后者已經是最新的了,所以無須執行,所以得到了如上的輸出。

為了驗證 phony 類型任務是否每次都執行,向 allmain.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⏎

相關資源


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM