Makefile 用法
基本用法
直接在命令行執行 make
命令即可。
make
命令會在當前目錄下查找 makefile
文件,找到後就去解析該文件中的第一個目標。最簡單的 makefile
內容如下:
# 要生成 main.exe 這個目標文件: 需要依賴 main.c 這個文件
main.exe: main.c
# 用於生成 main.exe 的命令
gcc main.c -o main.exe
make
命令找到 makefile
之後,會解析其中的第一個目標 main.exe
,發現該目標需要 main.c
文件,就在當前目錄中查找 main.c
文件,如果找到,就執行後面的 gcc main.c -o main.exe
來生成 main.exe
,如果找不到,就報錯。
我們也可以在 makefile
中指定 main.c
文件的生成方式,比如:
# 要生成 main.exe 這個目標文件: 需要依賴 main.c 這個文件
main.exe: main.c
# 用於生成 main.exe 的命令
gcc main.c -o main.exe
# 要生成 main.c 這個文件: 不需要依賴任何其它文件
main.c:
# 用於生成 main.c 的命令
echo "int main() { return 0; }" > main.c
這樣,當 make
程序找不到 main.c
文件時,就會在 makefile
中找 main.c
目標,找到後,就通過該目標生成 main.c
文件,然後再繼續編譯 main.exe
文件。
格式要求
makefile
以製表符(tab)作爲命令的識別符號,所以任何命令前面都必須有一個製表符,不能使用空格代替。另外,註釋前面不要添加製表符,否則會被當作命令執行。這裏的命令就是任何可以在命令行中執行的命令。
執行原則
如果一個目標所對應的文件已經存在,並且該目標的所有依賴項都沒有發生變更,則該目標中的命令不會被執行。比如上面的例子,如果 main.exe
已經存在,並且 main.c
沒有被修改,則 make
命令將不會做任何操作。
make
通過對比 main.exe
和 main.c
的“最後修改時間”來判斷 main.c
是否被修改,如果 main.c
比 main.exe
新,則判斷爲 main.c
被修改,否則未被修改。
多個目標
一個目標可以依賴多個其它目標,各個依賴之間用空格隔開。
任何一個目標的名稱都可以作爲參數傳遞給 make
,讓 make
從指定的目標開始解析:
# 依賴多個目標
main.exe: main.c foo.o bar.o
gcc main.c foo.o bar.o -o main.exe
foo.o: foo.c
gcc -c foo.c
bar.o: bar.c
gcc -c bar.c
# 清理臨時文件
clean:
# - 表示遇到錯誤不要停止
# @ 表示不顯示所執行的命令(不顯示 rm *.o)
-@rm *.o
執行 make clean
可以讓 make
從 clean
開始執行,而不是從 main
開始執行。
三個測試文件的內容如下:
// main.c
#include <stdio.h>
void foo();
void bar();
int main() {
printf("hello main\n");
foo();
bar();
return 0;
}
// foo.c
#include <stdio.h>
void foo() {
printf("hello foo\n");
}
// bar.c
#include <stdio.h>
void bar() {
printf("hello bar\n");
}
使用變量
# 定義變量
CC = gcc
main.exe: main.c foo.o bar.o
$(CC) main.c foo.o bar.o -o main.exe
foo.o: foo.c
$(CC) -c foo.c
bar.o: bar.c
$(CC) -c bar.c
clean:
rm *.o
指定輸出目錄
下面的腳本將臨時文件輸出到 output 目錄中統一存放:
main.exe: main.c output/foo.o output/bar.o
gcc main.c output/foo.o output/bar.o -o main.exe
# 依賴 output 目錄
output/foo.o: output foo.c
gcc -c foo.c -o output/foo.o
# 依賴 output 目錄
output/bar.o: output bar.c
gcc -c bar.c -o output/bar.o
# 創建 output 目錄
output:
-@mkdir output
clean:
-@rm output/*.o
注意“目標名稱”中也需要指定路徑,如果寫成下面這樣,雖然也能使用,但每次都會重新編譯 foo.o
和 bar.o
這兩個文件:
main.exe: main.c foo.o bar.o
gcc main.c output/foo.o output/bar.o -o main.exe
# 依賴 output 目錄
foo.o: output foo.c
gcc -c foo.c -o output/foo.o
# 依賴 output 目錄
bar.o: output bar.c
gcc -c bar.c -o output/bar.o
# 創建 output 目錄
output:
-@mkdir output
clean:
-@rm output/*.o
因爲 make
命令無法在當前目錄中找到 foo.o
和 bar.o
這兩個文件,所以每次都會執行 foo.o
和 bar.o
這兩個目標中的命令。
執行子目錄中的 makefile
修改 makefile
,內容如下:
# 依賴 foo bar 兩個目標,注意 output 要在 foo bar 的後面
main.exe: main.c foo bar output
gcc main.c output/foo.o output/bar.o -o main.exe
# 創建輸出目錄
output:
-@mkdir output
# 將 foo bar 兩個目標合在一起寫,因爲它們的命令相同
# 依賴一個永遠不存在的文件 null,相當於讓本目標每次都執行
foo bar: null
# 這裏的 $@ 就是目標名稱 foo bar,相當於 make -C foo bar
# 依次解析 for bar 中的 makefile 文件
-@make -C $@
# 一個永遠不存在的文件
null:
將前面的 foo.c
和 bar.c
分別放入 foo
和 bar
兩個子目錄中,然後在兩個子目錄中各創建一個 makefile
來編譯各自的文件,內容如下:
# foo/makefile 的內容
OUTPUT = ../output
# 生成 ../output/foo.o: 依賴 ../output 和 foo.c
$(OUTPUT)/foo.o: $(OUTPUT) foo.c
gcc -c foo.c -o $(OUTPUT)/foo.o
# bar/makefile 的內容
OUTPUT = ../output
# 生成 ../output/bar.o: 依賴 ../output 和 bar.c
$(OUTPUT)/bar.o: $(OUTPUT) bar.c
gcc -c bar.c -o $(OUTPUT)/bar.o
在這個例子中,foo
、bar
兩個目錄的“修改時間”比最後生成的 main.exe
要舊,按道理說,再次執行 make
時,main.exe
不會被重新編譯,所以也不會再執行 foo bar:
中的命令了,但事實上,每次執行 make
時都會執行 foo bar:
中的命令,但 main.exe
卻不會被重新編譯,個人理解原因如下:
main.exe:
依賴foo
、bar
,所以要去檢查foo bar:
是否需要更新。- 檢查發現
foo bar:
所依賴的文件null
不存在,所以導致null
需要被更新。 null
更新後,還要將foo
、bar
與null
做新舊比較,看foo
、bar
是否需要更新。結果發現null
還是不存在,就默認foo
、bar
比null
舊,需要更新。- 所以就執行
-@make -C $@
去更新foo bar:
,導致foo
、bar
兩個目錄中的makefile
被解析,由於foo.c
和bar.c
沒有被更改,所以此次解析沒有改變任何內容。解析完畢後foo bar:
就算更新完了。 - 然後還需要將
main.exe
和foo
、bar
做新舊比較,而foo
、bar
的時間戳沒有發生變化,所以main.exe
依然比foo
、bar
新,結果導致main.exe
不會被更新。
# 2 -- foo bar 沒有發生變化,所以 main.exe 不需要更新
main.exe: main.c foo bar output
gcc main.c output/foo.o output/bar.o -o main.exe
output:
-@mkdir output
# 1 -- null 不存在,所以 foo bar 需要更新,更新後 foo bar 也沒變化
foo bar: null
-@make -C $@
null:
需要注意的是,在 Windows
中,修改了 foo.c
或 bar.c
的內容後,foo
和 bar
兩個目錄的時間戳並不會發生變化,只有當 foo.c
或 bar.c
被重新編譯到 output
目錄中時,output
的時間戳纔會發生變化,而 foo
和 bar
的時間戳依然沒有變化,所以在上面的 makefile
中,需要將 main.exe:
的依賴項 output
放到 foo
和 bar
之後,以保證 foo
和 bar
中的內容被編譯後,能及時在 output
中被檢測到。
向子目錄傳遞變量
OUTDIR = output
# 導出 OUTDIR,讓子目錄中的 makefile 也可以使用該變量
export OUTDIR
main.exe: main.c $(OUTDIR) foo bar
gcc main.c $(OUTDIR)/foo.o $(OUTDIR)/bar.o -o main.exe
$(OUTDIR):
-@mkdir $(OUTDIR)
foo bar: null
-@make -C $@
null:
# foo/makefile 的內容
OUTPUT = ../$(OUTDIR)
# 生成 $(OUTPUT)/foo.o: 依賴 $(OUTPUT) 和 foo.c
$(OUTPUT)/foo.o: $(OUTPUT) foo.c
gcc -c foo.c -o $(OUTPUT)/foo.o
# bar/makefile 的內容
OUTPUT = ../$(OUTDIR)
# 生成 $(OUTPUT)/bar.o: 依賴 $(OUTPUT) 和 bar.c
$(OUTPUT)/bar.o: $(OUTPUT) bar.c
gcc -c bar.c -o $(OUTPUT)/bar.o
其它用法
bin/main.exe: bin foo bar foo bar
# 目標名稱
@echo $@
# 目標的目錄部分
@echo $(@D)
# 目標的文件名部分
@echo $(@F)
# 所有依賴(保留重複部分)
@echo $+
# 所有依賴(去掉重複部分)
@echo $^
# 第一個依賴
@echo $<
# 所有比目標新的依賴
@echo $?
bin:
foo:
bar:
# 查找當前目錄中的所有 *.c 文件,存入變量 FILES 中
FILES = $(wildcard *.c)
# 將 FILES 中的文件後綴 .c 改爲 .o 存入變量 FILES1 中
FILES1 = $(patsubst %.c, %.o, $(FILES))
# 去掉 FILES 中的文件後綴 .c 存入變量 FILES2 中
FILES2 = $(patsubst %.c, %, $(FILES))
# 顯示變量
echo:
@echo $(FILES)
@echo $(FILES1)
@echo $(FILES2)
# 查找當前目錄中的所有 *.c 文件,並將後綴 .c 改爲 .o 存入 FILES 中
FILES = $(patsubst %.c, %.o, $(wildcard *.c))
# 相當於 main.o: main.c
$(FILES): %.o : %.c
@echo $@
@echo $+
OUTPUT = output
SUBDIR = foo bar
all:
# 遞歸執行子目錄中的 make
@for i in $(SUBDIR); do make -C $$i; done
clean:
-@rm $(OUTPUT) -R
# 遞歸執行子目錄中的 make clean
@for i in $(SUBDIR); do make clean -C $$i; done