1.前言
最近在看Android源碼中涉及到了大量的Makefile文件,想通過這篇文章的學習掃如何編寫一個簡單的makefile文件,在後續的學習過程中,如果還有其他問題可以直接去官網繼續學習,國內的教程還有一個陳皓大神寫的《跟我一起寫Makefile》也是很經典的學習資料。
2.Makefile的由來
通常我們編寫項目的時候,都會編寫多個C文件,一個C文件我們可以編譯爲一個目標文件,多個目標文件可以組成一個程序。通過下面的例子,我們理解這一過程。
2.1代碼用例
- main.c
#include <stdio.h>
#include <stdlib.h>
int main(int argc,char *argv[])
{
int x,y ;
sscanf(argv[1],"%u",&x);
sscanf(argv[2],"%u",&y);
printf("func1:%u\n",func1(x,y));
printf("func2:%u\n",func2(x,y));
return 0;
}
- func1.c
#include <stdio.h>
int func1(int x,int y)
{
return x+y;
}
- func2.c
#include <stdio.h>
int func2(int x,int y)
{
return x*y;
}
2.1.1 編譯和執行
我們使用gcc
命令對上面的main.c
,fun1.c
,func2.c
三個文件編譯爲程序main
,並執行main
程序。
$ gcc main.c func1.c func2.c -o main
$ ./main 3 5
func1:8
func2:15
爲了簡化gcc的操作,我們通過make程序替代了gcc的編譯操作,這就是Makefile的由來。
2.2 make的工作方式
GNU 的 make 工作時的執行步驟如下:(想來其它的 make 也是類似)
-
讀入所有的 Makefile。
-
讀入被 include 的其它 Makefile。
-
初始化文件中的變量。
-
推導隱晦規則,並分析所有規則。
-
爲所有的目標文件創建依賴關係鏈。
-
根據依賴關係,決定哪些目標要重新生成。
-
執行生成命令。
1-5 步爲第一個階段,6-7 爲第二個階段。第一個階段中,如果定義的變量被使用了,那麼,make 會把其展開在使用的位置。但 make 並不會完全馬上展開,make 使用的是拖延戰術,如果變量出現在依 賴關係的規則中,那麼僅當這條依賴被決定要使用了,變量纔會在其內部展開。當然,這個工作方式你不一定要清楚,但是知道這個方式你也會對 make 更爲熟悉。有了這個基礎, 後續部分也就容易看懂了。
3.makefile如何編寫
通過上面的用例,我們把gcc的編譯操作移植到了makefile文件中去描述編譯的過程。
3.1 文件命名規則
在make程序中,我們的makefile文件的命名,只有兩種規則全部字母小寫
或者只有首字母大寫
,即makefile或者Makefile。
3.2 編寫規則
在講述這個makefile之前,還是讓我們先來粗略地看一看makefile的規則。
target ... : prerequisites ...
command //前面是一個tab符號,需要注意
...
...
-
target
可以是一個object file(目標文件),也可以是一個執行文件,還可以是一個標籤(label)。對 於標籤這種特性,在後續的“僞目標”章節中會有敘述。 -
prerequisites
生成該target所依賴的文件和/或target -
command
該target要執行的命令(任意的shell命令)
這是一個文件的依賴關係,也就是說,target這一個或多個的目標文件依賴於prerequisites中的文件, 其生成規則定義在command中。說白一點就是說:
prerequisites中如果有一個以上的文件比target文件要新的話,command所定義的命令就會被執行。
這就是makefile的規則,也就是makefile中最核心的內容。按照上面的規則,我們來編寫makefile
文件:
1 #func makefile
2 main: main.c fun1.c func2.c
3 gcc main.c func1.c func2.c -o main
執行日誌如下:
$ make
gcc main.c func1.c func2.c -o main
$ ./main 5 5
func1:10
func2:25
可以看出當我們執行make
命令時,gcc的編譯命令也被執行了。接下來,我們將文件改變一下。
3.3 多目標文件創建一個程序
上面的命令,我們將c文件直接編譯爲了main
程序,期間並沒有生成目標文件,這裏我們將文件修改下,生成目標文件。
main:main.o func1.o func2.o
gcc main.o func1.o func2.o -o main
main.o:main.c
gcc -c main.o
func1.o:func1.c
gcc -c func1.o
func2.o:func2.c
gcc -c func2.o
執行日誌
$ make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
$ ./main 5 6
func1:11
func2:30
- 當我們生成main程序的時候,需要main.o,func1.o,func2.o
- 而main.o是通過main.c生成的,這是一個遞歸生成目標文件的過程。
由此我們就可以得出結論,Makefile生成文件的過程就是一個遞歸的過程。
時間戳,在執行make命令的時候,如果make沒有改動是不會去編譯的。
3.4 添加多個功能
平常我們再make後,會加上清理,安裝,卸載的功能,都是通過僞目標的語法來編寫的,主要就是target:後面不編寫需要依賴的庫,類似函數名稱的概念。
#func makefile
main:main.o func1.o func2.o
gcc main.o func1.o func2.o -o main
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
clean:
rm func1.o func2.o main.o main
install:
cp main /usr/local/main
uninstall:
rm /usr/local/main
執行日誌:
$ sudo make install
cp main /usr/local/main
$ main 5 5
-bash: main: command not found
$ sudo make uninstall
rm /usr/local/main
$ make clean
rm func1.o func2.o main.o main
在上面執行日誌當中,我們已經看見安裝,卸載,清理功能都執行了。下面,我們來學習一些makefile的語法。
4.正式學習Makefile
在makefile的語法當中包含了一些我們在其他語言中基本包含的東西比如語法函數和控制語法,下面我們來編寫一些用例。
4.1 makefile的變量
makefile的變量包含三類:
- 用戶自定義變量
- 預定義變量
- 自動變量及環境變量
4.1.1 用戶自定義變量
變量在聲明時需要給予初值,而在使用時,需要給在變量名前加上 $ 符號,但最好用小括號 () 或是大括號 {} 把變量給包括起來。如果你要使用真實的 $ 字符,那麼你需要用 $$ 來表示。
變量可以使用在許多地方,如規則中的“目標”、“依賴”、“命令”以及新的變量中。
我們接着上一個例子編寫,將main.o func1.o func2.o
複製給MObj變量,使用時用$(MObj)
進行替代。
#func makefile
MObj=main.o func1.o func2.o
main:$(MObj)
gcc $(MObj) -o main
main.o:main.c
gcc -c main.c
func1.o:func1.c
gcc -c func1.c
func2.o:func2.c
gcc -c func2.c
clean:
rm $(MObj) main
install:
cp main /usr/local/main
uninstall:
rm /usr/local/main
執行日誌如下:
$ make
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
$ ./main 5 5
func1:10
func2:25
另外的變量複製方式
上面使用=
號來進行賦值,會將main.o進行遞歸的生成,如果我們想去掉此功能,可以使用:=
符號來進行賦值。這也是我們常用的賦值方式。
4.1.2 預定義變量
4.1.3 預定義變量
根據main:main.o func1.o func2.o
這句語法看見,我們的目標文件main
的生成是依靠main.o
、func1.o
、func2.o
文件生成的,通過上表的預定義變量,我們的指令中$^
代表所有不重複的依賴文件main.o
、func1.o
、func2.o
,$@
代表生成的目標文件main
,可以改成下面圖片的樣子.
4.2 僞目標
最早先的一個例子中,我們提到過一個“clean”的目標,這是一個“僞目標”.
clean:
rm *.o temp
“僞目標”並不是一個文件,只是一個標籤,由於“僞目 標”不是文件,所以 make 無法生成它的依賴關係和決定它是否要執行。爲了避免和文件重名的這種情況,我們可以使用一個特殊的標記“.PHONY”來顯式地指明一 個目標是“僞目標”,向 make 說明,不管是否有這個文件,這個目標就是“僞目標”。
.PHONY : clean
只要有這個聲明,不管是否有“clean”文件,要運行“clean”這個目標,只有“make clean”這 樣。於是整個過程可以這樣寫:
.PHONY : clean
clean:
rm $(MObj) main
參考鏈接:https://seisman.github.io/how-to-write-makefile/rules.html#id6
4.3 引用其它make file及makefile嵌套
4.3.1 引用其它makefile
在 Makefile 使用 include 關鍵字可以把別的 Makefile 包含進來,這很像 C 語言的 #include ,被包含的文件會原模原樣的放在當前文件的包含位置。include 的語法是:
include <filename>
在 include 前面可以有一些空字符,但是絕不能是 Tab 鍵開始。
- 例子:
#func makefile
include config.mk
MObj=main.o func1.o func2.o
...
在同級目錄下,創建一個config.mk
文件,在表頭用include config.mk
語法包含。
如果你想讓 make 不理那些無法讀取的文件,而繼續執行,你可以 在 include 前加一個減號“-”.
4.3.2 嵌套其它文件
在一些大的工程中,我們會把我們不同模塊或是不同功能的源文件放在不同的目錄中,我們可以在 每個目錄中都書寫一個該目錄的 Makefile,這有利於讓我們的 Makefile 變得更加地簡潔,而不至於 把所有的東西全部寫在一個 Makefile 中,這樣會很難維護我們的 Makefile,這個技術對於我們模塊 編譯和分段編譯有着非常大的好處。
例如,我們有一個子目錄叫 subdir,這個目錄下有個 Makefile 文件,來指明瞭這個目錄下文件的 編譯規則。那麼我們總控的 Makefile 可以這樣書寫:
subsystem:
cd subdir && $(MAKE)
其等價於:
subsystem:
$(MAKE) -C subdir
定義$(MAKE)宏變量的意思是,也許我們的make需要一些參數,所以定義成一個變量比較利於維護。這兩個 例子的意思都是先進入“subdir”目錄,然後執行make命令。
我們把這個Makefile叫做“總控Makefile”,總控Makefile的變量可以傳遞到下級的Makefile中(如果 你顯示的聲明),但是不會覆蓋下層的Makefile中所定義的變量,除非指定了 -e 參數。
參考鏈接:https://seisman.github.io/how-to-write-makefile/recipes.html#make
4.4 條件判斷
使用條件判斷,可以讓make根據運行時的不同情況選擇不同的執行分支。條件表達式可以是比較變量的值, 或是比較變量和常量的值。
我們將上面main的例子改變下,判斷 $(CC) 變量是否 gcc ,如果是的話,則使用GNU函數編譯目標。
MObj=main.o func1.o func2.o
CFLAGS= -c
CC=gcc
main:$(MObj)
ifeq ($(CC),gcc)
gcc $^ -o $@
else
$(CC) $^ -o main2
endif
....
這裏注意語法,條件判斷需要頂頭寫。
參考鏈接:https://seisman.github.io/how-to-write-makefile/conditionals.html
函數
在Makefile中可以使用函數來處理變量,從而讓我們的命令或是規則更爲的靈活和具有智能。make 所支持的函數也不算很多,不過已經足夠我們的操作了。函數調用後,函數的返回值可以當做變量來使用。
函數調用,很像變量的使用,也是以 $ 來標識的,其語法如下:
$(<function> <arguments>)
或是:
${<function> <arguments>}
這裏, <function>
就是函數名,make支持的函數不多。 爲函數的參數, 參數間以逗號 , 分隔,而函數名和參數之間以“空格”分隔。函數調用以 $ 開頭,以圓括號 或花括號把函數名和參數括起。感覺很像一個變量,是不是?函數中的參數可以使用變量,爲了風格的 統一,函數和變量的括號最好一樣,如使用 $(subst a,b,$(x))
這樣的形式,而不是 $(subst a,b, ${x})
的形式。因爲統一會更清楚,也會減少一些不必要的麻煩。
更過細節請查看鏈接:https://seisman.github.io/how-to-write-makefile/functions.html
Makefile的管理命令
-C dir
:讀入制定目錄下面的makefile
make -C <包含makefile文件的目錄>
示例
$ ls
cpp
$ make -C cpp
make: Entering directory `/home/mujf/workspace/cpp'
gcc -c main.c
gcc -c func1.c
gcc -c func2.c
gcc main.o func1.o func2.o -o main
make: Leaving directory `/home/mujf/workspace/cpp'
我們回退到cpp的同級目錄中,執行命令,會自動進入cpp目錄並且執行make命令,在Android源碼編譯中被大量使用。
-f file
讀入當前目錄下的file文件爲makefile
make -f makefile
表示執行當前文件夾下指定的makefile
文件。
-i
忽略所有命令執行錯誤-I dir
指定被包含的makefile所在目錄
這裏是大i
結束語
這裏,我們已經可以開始研究Android的源碼,在源碼的build/core
路徑下包含了大量的makefile文件。接下來閱讀源碼,我們需要思考三個問題:
- 源文件或者依賴文件過多怎麼辦?
makefile文件分開或者分級
- output不僅僅一個文件怎麼辦?
用多個makefile文件,互相include 嵌套
使用僞目標 make all
- 如何嵌套執行makefile文件
可查看上面的內容