【linux】-Makefile簡要知識+一個通用Makefile


Makefile

 meke命令一般用於編譯程式,而make命令都依賴於 Makefile 文件。
 最簡單的Makefile如下:

hello:  hello.c
   gcc -o hello hello.c
clean:
   rm -f hello

註:縮進使用 tab 鍵 , 因為Makefile 會為每個以 tab 鍵開頭的命令創建一個 shell 去執行。
 具體規則參考 《GUN Make 使用手冊》

以下為粗略筆記

Makefile規則與示例

為什麼需要Makefile

 提高編譯效率。

Makefile樣式

 一個簡單的Makefile文件包含一系列”規則”:

目標...:依賴...
<tab>命令
  • 命令被執行的兩個簡單條件:
    • 目標文件還沒有生成
    • 依賴文件比目標文件新

先介紹Makefile的兩個函數

  1. $(foreach var,list,text)
     for each var in list,change it to text。
     對list中的每一個元素,取出賦值給var,然後把var改為text所描述的形式。
    例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)  // 最終 dep_files := .a.o.d .b.o.d
  1. $(wildcard pattern)
      把存在的 pattern 文件列出來
    例子:
src_files := $(wildcard *.c)

完善Makefile

  1. 先來一個簡單粗暴、效率低的:
test: main.c hello.c hello.h
        gcc -o test main.c hello.c
  1. 再來一個Makefile,效率高、精鍊,支援自動檢測頭文件
objs := main.o hello.o

test : $(objs)
    gcc -o test $^

# 需要判斷是否存在依賴文件
# .main.o.d   .hello.o.d
dep_files := $(foreach f, $(objs), .$(f).d)
dep_files := $(wildcard $(dep_files))

# 把依賴文件包含進來
ifneq ($(dep_files),)
include $(dep_files)
endif

%.o : %.c
    gcc -Wp,-MD,[email protected] -c -o $@ $<

clean:
    rm *.o test -f

distclean:
    rm $(dep_files) *.o test -f

了解自動化變數。

通用Makefile的使用

參考linux內核的Makefile來編寫一個通用的Makefile,特點:

  1. 支援多個目錄、多層目錄、多個文件;
  2. 支援給所有文件設置編譯選項;
  3. 支援給目錄設置編譯選項;
  4. 支援給某個文件單獨設置編譯選項;
  5. 簡單、好用。

通用的Makefile解釋

零星知識點
make命令的使用

 執行make命令時,它會去當前目錄下查找名為Makefile的文件,並根據它的只是去執行操作,生成第一個目標。
 也可以用 -f 選項指定文件,如:

make -f Makefile.build

指定文件的名字隨意定義


 可以使用 -C 指定目錄,切換到其他目錄去,如:

make   -C   a/   abc.def

 可以指定目標,不再默認生成第一個目錄,如:

make   -C   a/   abc.def   other_target
即時變數與臨時變數

 變數定義語法:

形式 說明
A = xxx 延時變數
B ?= xxx 延時變數,只有第一次定義時賦值才成功,若曾被定義過,則此賦值無效。
C := xxx 立即變數
D += yyy 如果D在前面是延時變數,那麼現在它還是延時變數
如果D在前面是立即變數,那麼它現在還是立即變數

延時變數
 使用時才確定該值。
如:

A = $@
tets:
    @echo $A

輸出 A 的值為 test
即時變數
 定義時立即確定該值。
如:

A := $@
tets:
    @echo $A

輸出 A 的值為

變數的導出(export)

export 是供給子目錄的 Makefile 使用的(即 sub-make),同一級的makefile是訪問不到的。
 可以通過makefile中的內置變數MAKELEVEL可以查看當前的makefile的level。

Makefile中的shell

 Makefile中可以使用shell命令,如:

TOPDIR := $(shell_pwd)
Makefile中放置第一個命令

 執行目標時,如果不指定目標,則默認執行第一個目標。
所以,第一個目標的位置很重要。有時候不太方便把第一個目標完整地放在文件的前面,這時可以在文件的前面直接放置目標,在後面再完善它的依賴和命令。比如:

First_target:      // 這句話放在前面
........               // 其他程式碼,比如include其它文件得到後的 xxx 變數
First_target : $(xxx) $(xxx)
    command
假想目標

Makefile中可能會有這樣的目標:

clean:
    rm     -f     $(shell find -name "*.o")
    rm    -f      $(TARGET)

:如果當前目錄下更好有個名為「clean」的文件,那麼執行「make clean」時就不會執行makefile中目標clean的內容,所以要把clean設置為假想目標即可:

.PHONY    :   clean
小知識

PHONY 是 phoney,即偽造的意思。PHONY後面的target是偽造的target,不是真實存在的文件。同時,注意make命令中後面的target默認是文件。

常用函數
  • $(foreach var, list, text)
    for each var in list,change it to text.
    直接上例子:
objs := a.o b.o
dep_files := $(foreach f, $(objs), .$(f).d)   // dep_files 的最終結果為「**.a.o.d .b.o.d**」

  • $(wildcard pattern)
    列出 pattern 存在的文件。
    例子:
src_files = $(wildcard *.c) // src_files的值為當前目錄下所有 .c 文件。

  • $(filter pattern…, text)
    text中符合pattern格式的內容留下來
obj-y := a.o b.o c/ d/
DIR := $(filter %/, $(obj-y))   //結果為:c/ d/

  • $(filter-out pattern…, text)
    text中符合pattern格式的內容刪除掉
obj-y := a.o b.o c/ d/
DIR := $(filter-out %/, $(obj-y))   // 結果為:a.o b.o

  • $(patsubst pattern, replacement, text)
    尋找text中符合pattern格式的內容,用replacement代替他們。
subdir-y := a.o b.o c/ d/
subdir := $(patsubst %/, %, $(obj-y))   // 結果為:c d

*通用Makefile設計思想
  1. 在Makefile文件中確定要編譯的文件、目錄,比如:
obj-y += main.o
obj-y += a/

Makefile 文件總是被 Makefile.build 包含的。

  1. 在 Makefile.build 中設置編譯規則,有 3 條編譯規則
    1. 怎麼編譯子目錄?
      1. 進入子目錄編譯即可:
$(subdir-y):
    make -C $@ -f $(TOPDIR)/Makefile.build
2. 如何編譯當前目錄中的目標文件?
%.o : %.c
    $(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
3. 當前目錄下的 **.o** 和子目錄下的 **built-in.o** 要打包起來:
$(TARGET) : built-in.o
    $(CC) $(LDFLAGS) -o $(TARGET) built-in.o

思想總結

  1. Makefile文件確定要編譯的文件和目錄
  2. Makefile.build文件包含規則
  3. 每個需要編譯的子目錄都放個Makefile文件,把需要編譯的文件編譯成built-in.o
  4. 再把所有子目錄的built-in.o鏈接到頂層的built-in.o
  5. 最後把頂層built-in.o鏈接到APP

最好重新分析一下通用Makefile

一個通用Makefile

本程式的Makefile分為3類:

  1. 頂層目錄的Makefile
  2. 頂層目錄的Makefile.build
  3. 各級子目錄的Makefile

源碼(注釋版)

Makefile

CROSS_COMPILE = # 交叉編譯工具頭,如:arm-linux-gnueabihf-
AS      = $(CROSS_COMPILE)as # 把彙編文件生成目標文件
LD      = $(CROSS_COMPILE)ld # 鏈接器,為前面生成的目標程式碼分配地址空間,將多個目標文件鏈接成一個庫或者一個可執行文件
CC      = $(CROSS_COMPILE)gcc # 編譯器,對 C 源文件進行編譯處理,生成彙編文件
CPP    = $(CC) -E
AR      = $(CROSS_COMPILE)ar # 打包器,用於庫操作,可以通過該工具從一個庫中刪除或則增加目標程式碼模組
NM     = $(CROSS_COMPILE)nm # 查看靜態庫文件中的符號表

STRIP       = $(CROSS_COMPILE)strip # 以最終生成的可執行文件或者庫文件作為輸入,然後消除掉其中的源碼
OBJCOPY  = $(CROSS_COMPILE)objcopy # 複製一個目標文件的內容到另一個文件中,可用於不同源文件之間的格式轉換
OBJDUMP = $(CROSS_COMPILE)objdump # 查看靜態庫或則動態庫的簽名方法

# 共享到sub-Makefile
export AS LD CC CPP AR NM
export STRIP OBJCOPY OBJDUMP

# -Wall : 允許發出 GCC 提供的所有有用的報警資訊
# -O2 : 「-On」優化等級
# -g : 在可執行程式中包含標準調試資訊
# -I : 指定頭文件路徑(可多個)
CFLAGS := -Wall -O2 -g 
CFLAGS += -I $(shell pwd)/include

# LDFLAGS是告訴鏈接器從哪裡尋找庫文件,這在本Makefile是鏈接最後應用程式時的鏈接選項。
LDFLAGS := 

# 共享到sub-Makefile
export CFLAGS LDFLAGS

# 頂層路徑
TOPDIR := $(shell pwd)
export TOPDIR

# 最終目標
TARGET := test

# 本次整個編譯需要源 文件 和 目錄
# 這裡的「obj-y」是自己定義的一個格式,和「STRIP」這些一樣,*但是 一般內核會搜集 」obj-」的變數*
obj-y += main.o # 需要把當前目錄下的 main.c 編進程式里
obj-y += sub.o # 需要把當前目錄下的 sub.c 編進程式里
obj-y += subdir/ # 需要進入 subdir 這個子目錄去尋找文件來編進程式里,具體是哪些文件,由 subdir 目錄下的 Makefile 決定。
#obj-y += $(patsubst %.c,%.o,$(shell ls *.c))

# 第一個目標
all : start_recursive_build $(TARGET)
    @echo $(TARGET) has been built !
    
# 處理第一個依賴,**轉到 Makefile.build 執行**
start_recursive_build:
    make -C ./ -f $(TOPDIR)/Makefile.build
    
# 處理最終目標,把前期處理得出的 built-in.o 用上
$(TARGET) : built-in.o
    $(CC) -o $(TARGET) built-in.o $(LDFLAGS)
    
# 清理
clean:
    rm -f $(shell find -name "*.o")
    rm -f $(TARGET)
    
# 徹底清理
distclean:
    rm -f $(shell find -name "*.o")
    rm -f $(shell find -name "*.d")
    rm -f $(TARGET)

Makefile.build

注意,include 命令,相對路徑為 執行本 Makefile.build 的路徑

# 偽目標
PHONY := __build
__build:

# 清空需要的變數
obj-y :=
subdir-y :=
EXTRA_CFLAGS :=

# 包含同級目錄Makefile
# 這裡要注意,相對路徑為 執行本 Makefile.build 的路徑
include Makefile

# 獲取當前 Makefile 需要編譯的子目錄的目錄名
# obj-y := a.o b.o c/ d/
# $(filter %/, $(obj-y))   : c/ d/
# __subdir-y  : c d
# subdir-y    : c d
__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y	+= $(__subdir-y)

# 把子目錄的目標定為以下注釋
# built-in.o d/built-in.o
subdir_objs := $(foreach f,$(subdir-y),$(f)/built-in.o)

# 獲取當前目錄需要編進程式的文件名作為,並寫為目標
# a.o b.o
cur_objs := $(filter-out %/, $(obj-y))
# 使修改頭文件 .h 後,重新make後可以重新編譯(重要)
dep_files := $(foreach f,$(cur_objs),.$(f).d)
# 列出存在的文件
dep_files := $(wildcard $(dep_files))

ifneq ($(dep_files),)
  include $(dep_files)
endif


PHONY += $(subdir-y)

# 第一個目標
__build : $(subdir-y) built-in.o

# 優先編譯 子目錄的內容
$(subdir-y):
	make -C $@ -f $(TOPDIR)/Makefile.build

# 鏈接成 目標
built-in.o : $(cur_objs) $(subdir_objs)
	$(LD) -r -o $@ $^

dep_file = [email protected]

# 生成 cur_objs 目標
%.o : %.c
	$(CC) $(CFLAGS) $(EXTRA_CFLAGS) $(CFLAGS_$@) -Wp,-MD,$(dep_file) -c -o $@ $<
	
.PHONY : $(PHONY)

使用說明

  • 本程式的Makefile分為3類:
    1. 頂層目錄的 Makefile
    2. 頂層目錄的 Makefile.build
    3. 各級子目錄的 Makefile

1. 各級子目錄的Makefile

  • 參考:
# 可以不添加下面兩個變數
EXTRA_CFLAGS  := 
CFLAGS_test.o := 

obj-y += test.o
obj-y += subdir/
  • obj-y += file.o 表示把當前目錄下的file.c編進程式里,
  • obj-y += subdir/ 表示要進入subdir這個子目錄下去尋找文件來編進程式里,是哪些文件由subdir目錄下的Makefile決定。
  • EXTRA_CFLAGS, 它給當前目錄下的所有文件(不含其下的子目錄)設置額外的編譯選項, 可以不設置
  • CFLAGS_xxx.o, 它給當前目錄下的xxx.c設置它自己的編譯選項, 可以不設置
注意
  1. “subdir/”中的斜杠”/”不可省略
  2. 頂層Makefile中的CFLAGS在編譯任意一個.c文件時都會使用
  3. CFLAGS EXTRA_CFLAGS CFLAGS_xxx.o 三者組成xxx.c的編譯選項

2. 頂層目錄的Makefile

主要作用:

  • 整個工程的參數初期定義
    • 架構
    • 工具鏈
    • 編譯工具
    • 編譯參數
    • 需要導出的變數
    • 等等
  • 定義最終目標
  • 跳轉到 Makefile.build

3. 頂層目錄的Makefile.build **

注意:該文件雖然放在頂層,但是也是提供給各級子目錄使用的。
主要功能:

  • 把某個目錄及它的所有子目錄中、需要編進程式去的文件都編譯出來,把各個subdir/built-in.o和當前目錄的目標 .o 合併打包為當前目錄的built-in.o

使用提示

  • 執行”make”來編譯,執行 make clean 來清除,執行 make distclean 來徹底清除。
Makefile附件
一些符號
符號 說明
$@ 表示規則中的目標文件集
$% 當目標為函數庫的時候,則表示規則中的目標成員名。反之為空。如一個目標為”foo.a(bar.o)“,那麼,”$%”就是”bar.o”,以空格分隔開。
$< 依賴文件集合中的第一個文件,如果依賴文件以”%“形式出現,則表示符合模式的一系列文件集合
$? 所有比目標新的依賴集合,以空格分隔開。
$^ 所有依賴文件集合,以空格分隔開。如果依賴有相同,則取其一。
$+ 和 “$^”類同,但是不會把相同的刪除掉。
$* 這個變數表示目標模式中 “%”及其之前的部分,如果目標是 test/a.test.c,目標模式為 a.%.c, 那麼 “$* ” 就是 test/a.test。

參考

  • 以上筆記為整理韋東山老師筆記而來的