Linux開發之Makefile簡明教程及示例

飛鶴0755發表於2020-12-22

前言

Linux下的開發,一般都是基於開源的編譯器,很多時候並沒有太好的IDE,此時非常有必要掌握一門通用的編譯構建方法。Makefile應運而生,成功了最流行的Linux下的編譯構建方法。Makefile主要是面向程式碼的編譯構建,但是其目標依賴執行的基本原理,也可以用到其他有類似的場景。

1. 概念

1.1. Makefile是什麼?

Makefile是一個有規則要求的描述檔案,然後通過make工具來解析並執行其中描述的動作。Makefile可以呼叫相應電腦的命令列來執行一些動作,所以除了不同作業系統的命令列有一些差異外,Makefile的基本規則在所有作業系統中都是一樣的。

1.2. Makefile的基本內容

Makefile主要包括兩部分,一是變數的定義及展開,另外就是目標和依賴及執行。

1.3. Makefile執行的過程

當我們執行make時,make會在當前目錄下找Makefile或makefile的檔案,找到之後,就開始解析和執行。因為其根據時間戳來判定是否執行,所以可以達到只編譯修改過的原始檔,而不用重新編譯沒有修改過的的原始檔。

  • 解析,就是如果有#include指令,則將相應檔案新增進來,然後完成變數的定義及展開。
  • 執行,找到終極目標及其依賴,依賴預設是檔案,make會檢測依賴與目標的時間戳,如果依賴的時間戳比目標的時間戳更新,則開始執行相應的命令,否則不進行執行。此原理可以解決多檔案編譯時重複編譯的問題。

2. 變數

2.1. 變數的定義

> variable=value list

變數命名可是包含字元、數字、下劃線,不能含有":"、"#"、"=“或空字元,變數名大小寫敏感。變數名,類似C/C++中的巨集,所以推薦用全大寫加下劃線區分單詞。
值列表,值可以有多個,以空格分隔。

2.2. 變數的賦值

變數賦值有4種形式:

  • 簡單賦值 ( := ) 程式語言中常規理解的賦值方式,只對當前語句的變數有效。
    MAIN := main
    OBJ  := $(MAIN).o
    MAIN := wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    
    輸出:

    wmain
    main.o

從輸出可以看出,簡單賦值是有先後順序的。

  • 遞迴賦值 ( = ) 賦值語句可能影響多個變數,所有目標變數相關的其他變數都受影響。
    MAIN = main
    OBJ  = $(MAIN).o
    MAIN = wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    
    輸出:

    wmain
    wmain.o

從輸出可以看出,遞迴賦值後面的操作會影響前面的關聯變數。所以一般情況下,不建議使用遞迴賦值。

  • 條件賦值 ( ?= ) 如果變數未定義,則使用符號中的值定義變數。如果該變數已經賦值,則該賦值語句無效。

    MAIN := main
    OBJ  := $(MAIN).o
    MAIN ?= wmain
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    

    輸出:

    main
    main.o

  • 追加賦值 ( += ) 原變數用空格隔開的方式追加一個新值。

    MAIN := main
    OBJ  := $(MAIN).o
    MAIN += $(OBJ)
    test:
    	@echo $(MAIN)
    	@echo $(OBJ)
    

    輸出:

    main main.o
    main.o

  • 變數賦值的換行

    OBJS := main.o \
    		add.o \
    		sub.o \
    		comb.o
    

2.3. 變數的引用

變數的引用,即使用變數,使用$(variable)或 ${variable},並且變數可以巢狀引用。變數的引用是一個展開的過程,類似巨集的展開。

2.3.1. 基本引用及巢狀引用

	# file name:Makefile
	OBJ = main.o add.o
	NEW_OBJ = $(OBJ)
	ALIAS_OBJ = NEW_OBJ
	ALL_OBJ = $($(ALIAS_OBJ))
	
	# 命令列輸入make測試
	test:
		@echo $(OBJ)
		@echo $(NEW_OBJ)
		@echo $(ALIAS_OBJ)
		@echo $(ALL_OBJ)
	```

輸出:

main.o add.o
main.o add.o
NEW_OBJ
main.o add.o

2.3.2. 複雜引用

Makefile的變數引用的展開,不僅是單純的替換,還可以呼叫一些特殊的函式,使得展開可以完成更復雜的功能。

2.3.2.1. 萬用字元

萬用字元使用說明
*匹配0個或者是任意個字元
?匹配任意一個字元
[]我們可以指定匹配的字元放在 “[]” 中
	# 當前目錄下有a.c、b.c、ab.c3個原始檔
	SOURCE1 := *.c
	SOURCE2 := ?.c
	SOURCE3 := [ab].c
	
	test:
		@echo $(SOURCE1)
		@echo $(SOURCE2)
		@echo $(SOURCE3)
輸出
>a.c ab.c b.c
a.c b.c
a.c b.c

2.3.2.2. 特殊萬用字元

%,在變數展開過程中的模式萬用字元,表示匹配所有字元。

	DIR := A B
	OBJS := $(patsubst %, %.o, $(DIR))

	test:
		@echo $(OBJS)

輸出:

A.o
B.o

2.3.2.3. 函式

在變數引用展開的過程中,可以呼叫特殊函式。

  • patsubst,模式字串替換函式。$(patsubst %.c, %.o, $(SOURCES)),將 $(SOURCES)中所有.c結構的檔名替換為.o結尾。

  • subset,字串替換函式,$(subset AA, aa, AAbbcc),將大寫AA替換成小寫aa。

  • strip,去掉空格,$(strip )。

  • findstring,查詢字串函式,找到即返回目標字串,否則返回空。

  • filter,過濾函式,$(filter pattern…, text),$(filter %.c %.o,1.c 2.o 3.s),返回1.c 2.o。

  • filter-out,反過濾函式,$(filter-out pattern…,text)。

  • sort,去重排序函式,$(sort list)。

  • notdir,去掉目錄,保留檔名。

  • dir,取目錄函式。

  • suffix,取字尾副檔名函式,$(suffix names…)。

  • basename,去掉字尾函式,$(basename names…)。

  • addsuffix,新增擴充套件字尾函式,$(addsuffix suffix,names…)。

  • addprefix,新增字首函式,可以給檔案新增目錄,$(addprefix prefix,names…)。

  • join,連線函式,$(join list1, list2)。

  • wildcard,萬用字元函式,在函式中如果使用了萬用字元,需要用此函式標記,$(wildcard pattern)。

  • if,條件函式,$(if condition,then-part[,else-part])。

  • or,條件或函式,條件中有一個有效則返回,$(or condition1[,condition2[,condition3…]])。

  • and, 條件與函式,條件都有效,則返回最後一個字串,$(and condition1[,condition2[,condition3…]])。

  • foreach,遍歷列表提取指定元素並執行字串,$(foreach var,list,text)。

  • file,檔案操作函式,可以讀寫檔案,$(file op filename[,text])。

  • call,傳遞引數並呼叫新的變數展開,$(call variable,param,param,…)。

  • shell,呼叫shell命令函式,$(shell cmd param)。

    #獲取當前目錄
    CUR_DIR := $(shell pwd)
    # 提取當前資料夾名
    CUR_DIR_NAME := $(notdir $(shell pwd))
    # 獲取當前目錄下所有資料夾路徑,包括當前資料夾
    SOURCE_DIR	:= $(shell find $(CUR_DIR) -type d)
    #遍歷獲取當前目錄下所有.c檔案
    SOURCES		:= $(wildcard $(patsubst %, %/*.c, $(SOURCE_DIR)))
    # 將所有.c檔案列表轉為.o列表
    OBJS	:= $(SOURCES:.c=.o)
    test:
    	@echo $(CUR_DIR)
    	@echo $(CUR_DIR_NAME)
    	@echo $(SOURCE_DIR)
    	@echo $(OBJS)
    

2.4. 內建變數

  • AR,打包函式,預設命令為"ar"
  • AS,彙編編譯,預設命令為"as"
  • CC,C語言編譯,預設命令為"cc"
  • CXX,C++語言編譯,預設命令為"g++"
  • CPP,C語言預處理命令,預設為"$(CC) -E”
  • RM,刪除檔案命令,預設為"rm -f"
  • ARFLGS,打包程式AR的引數,預設為"rv"
  • ASFLAGS,組合語言編譯器引數
  • CFLGS,C語言編譯引數
  • CXXFLAGS,C++語言編譯器引數
  • LDFLAGS,連結器引數。
    Makefile的隱式規則會呼叫相應的內建變數,使用者如果沒有重新賦值,則用預設值。

3. 目標、依賴和命令

3.1. 基礎

目標可以1個,也可以多個。依賴可以沒有,也可以多個。命令可以1個,也可以多個。目標和依賴即可以是字元量,也可以是變數引用,甚至可以直接使用萬用字元描述。其規則如下:
注意:command前面必須以真實Tab鍵(不能是多個空格)隔開,標記後面的內容是命令。

target ... : prerequisites1 ...
	command
	...
	...
	```
prerequisites1 ...: prerequisites11 ...
	command
	...
	...
prerequisites11 ...:
	command
	...
	...

一般情況,目標和依賴,都被make程式當作檔案處理。make程式會識別出頂層目標,然後一層一層往下連結。

  • 當依賴檔案的時間戳新於目標檔案,則make程式會執行下面的命令,來通過依賴項構建新的目標。
  • 當依賴檔案的時間戳晚於目標檔案,則下面的命令不會執行。
  • 當目標檔案不存在時,則會呼叫命令.
  • 當依賴檔案不存在時,表明依賴檔案對應的目標需要構建執行,然後再執行當前目標對應的命令。
    當依賴檔案被識別到需要構建時,依賴必須作為目標存在,否則會提示相應目標沒有規則,報錯。

3.2. 多目標與多依賴

3.2.1. 自動化變數

在多目標和多依賴規則中,自動化變數可以自動指代相應變數。

自動化變數說明
$@表示規則的目標檔名。如果目標是一個文件檔案(Linux 中,一般成 .a 檔案為文件檔案,也成為靜態的庫檔案),那麼它代表這個文件的檔名。在多目標模式規則中,它代表的是觸發規則被執行的檔名 。
$%當目標檔案是一個靜態庫檔案時,代表靜態庫的一個成員名。
$<規則的第一個依賴的檔名。如果是一個目標檔案使用隱含的規則來重建,則它代表由隱含規則加入的第一個依賴檔案。
$?所有比目標檔案更新的依賴檔案列表,空格分隔。如果目標檔案時靜態庫檔案,代表的是庫檔案(.o 檔案)。
$^代表的是所有依賴檔案列表,使用空格分隔。如果目標是靜態庫檔案,它所代表的只能是所有的庫成員(.o 檔案)名。一個檔案可重複的出現在目標的依賴中,變數“ ” 只 記 錄 它 的 第 一 次 引 用 的 情 況 。 就 是 說 變 量 “ ^”只記錄它的第一次引用的情況。就是說變數“ ^”會去掉重複的依賴檔案。
$+類似“$^”,但是它保留了依賴檔案中重複出現的檔案。主要用在程式連結時庫的交叉引用場合。
$*在模式規則和靜態模式規則中,代表“莖”。“莖”是目標模式中“%”所代表的部分(當檔名中存在目錄時,“莖”也包含目錄部分)。

3.2.2. 普通多目標與多依賴

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

$(OBJS): FORCE
	@echo OBJS:$@

輸出:

OBJS:main.o
OBJS:add.o
main:main
main:main.o
main:main.o add.o

從示例中可以看出,$(OBJS)是遍歷執行的,每次提取一個目標$@,並執行命令。$<只提取依賴項中的第1個,$^則提取所有的依賴項。

3.2.3. 靜態模式規則

    <targets ...>: <target-pattern>: <prereq-patterns ...>
            <commands>
            ...

<targets …>:可以省略,target-pattern和prereq-patterns需要用到模式萬用字元%。%表示匹配當前目標集中的目標,並生成相應的依賴。

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

$(OBJS): %.o: %.c
	@echo OBJS:$@ $<

輸出:

OBJS:main.o main.c
OBJS:add.o add.c
main:main
main:main.o
main:main.o add.o

3.3. 偽目標

通常情況下,目標會被識別檔案,導致會觸發一些隱式編譯規則。有時為了避免這種情況,需要標識目標為偽目標,即不對應相應的檔案,並且不會被識別為頂層目標。
示例,使用make clean命令來刪除所有.o檔案。

.PHONE:clean
clean:
	rm -rf *.o

3.4. 隱式規則

當依賴項作為的目標不存在時,make會自動根據依賴項執行相應的命令。

OBJS := main.o add.o
main: $(OBJS)
	@echo main:$@
	@echo main:$<
	@echo main:$^

輸出

cc -c -o main.o main.c
cc -c -o add.o add.c
main:main
main:main.o
main:main.o add.o

make自動根據當前依賴項,查詢當前目標中是否有同名的原始檔,然後根據原始檔的字尾呼叫相應的預設編譯器變數來執行編譯。

3.5. 強制執行

有時想強勢執行目標,可以有下面三種方法。

  • 當目標檔案不存在時,會強制執行
test:
	@echo test
  • 當依賴檔案不存在時,會強制執行
test: FORCE
	@echo test

#當FORCE不存在時,會強制執行,習慣用這種方式
FORCE: ;
  • 當依賴項是偽目標時,會強制執行
test: FORCE
	gcc *.c

# 利用偽目標來強制執行	
.PHONE:FORCE
FORCE:;

4. 其他

4.1. 搜尋路徑

make預設是在當前目錄下搜尋相關檔案,但是有時可能存在相同檔名,或是需要特殊指定搜尋目錄時,需要有一種方法來指定優先搜尋目錄及檔案。

4.1.1. VPATH

VPATH是Makefile內建的環境變數,預設是空。當VPATH指定目錄時,make會優先從VPATH代表地目錄去搜尋相關檔案。

VPATH := src

表示優先從src目錄搜尋。

VPATH := src dll

表示先從src目錄搜尋,找不到再從dll目錄搜尋。

4.1.2. vpath

vpath是Makefile語法的關鍵字,語法如下:

vpath PATTERN DIRECTORIES
vpath PATTERN
vpath

# 表示從src目錄中搜尋test.c檔案
vpath test.c src
# 表示從src或dll目錄中搜尋test.c檔案
vapath test.c src dll
# 表示從當前目錄搜尋test.c,如果之前有相關設定,直接清除
vpath test.c
# 恢復預設,相當於清除之前所有設定
vapth

4.2. 條件判斷

關鍵字功能
ifeq判斷引數是否不相等,相等為 true,不相等為 false。
ifneq判斷引數是否不相等,不相等為 true,相等為 false。
ifdef判斷是否有值,有值為 true,沒有值為 false。
ifndef判斷是否有值,沒有值為 true,有值為 false。

語法形式:

ifeq (ARG1, ARG2)
ifeq 'ARG1' 'ARG2'
ifeq "ARG1" "ARG2"
ifeq "ARG1" 'ARG2'
ifeq 'ARG1' "ARG2"

如Windows下和Linux下的刪除命令不同,需要區分。

ifeq ($(OS),Windows_NT)
RM			:= del /q /f
else
RM 			:= rm -f
endif

4.3. 標頭檔案依賴

標頭檔案修改了,包括標頭檔案的原始檔則需要重新編譯。但是有時,原始檔中包含的標頭檔案較多,且標頭檔案中又引用其他標頭檔案。這樣複雜的情況下,手動建立一個原始檔和標頭檔案的依賴,不太容易。
gcc專門提供了一個編譯選項,供使用者提取原始檔引用的標頭檔案。

gcc -M main.c

會列出所有引用的標頭檔案,包括系統標頭檔案。系統標頭檔案不會改變,不需要建立依賴關係。

gcc -MM main.c > include.txt

會列出所有引用的非系統標頭檔案,並儲存到include.txt檔案中。
然後在Makefile中,通過include include.txt來將檔案中的內容引入Makefile中。

4.4. 巢狀呼叫Makefile

ALL_PRJ_OBJ :=  Public/ZLib \
				Public \
				Device \
				MPF	   \
				Main

MAIN_BIN 	:= Bin/main

all:$(ALL_PRJ_OBJ)
	@echo Complete!
	cp -r Bin ..
	
$(ALL_PRJ_OBJ): FORCE
	@cd $@ && make

# 強制更新
FORCE:;

.PHONY : clean run
clean:
	find . -name "*.o" -o -name "*.so" -o -name ".a" | xargs rm -rf
	rm -rf $(MAIN_BIN)

## 5. 示例

5.1. 編譯zlib為libzlib.so動態庫

# define the Cpp compiler to use
CXX = gcc

# 動態庫需要使用-fPIC
# define any compile-time flags
CXXFLAGS	:= -std=c99 -Wall -Wextra -g -fPIC 

# define library paths in addition to /usr/lib
#   if I wanted to include libraries not in /usr/lib I'd specify
#   their path using -Lpath, something like:
LFLAGS =

# define output directory
OUTPUT	:= ../../Bin

# define source directory
SRC		:= .

# define include directory
INCLUDE	:= ../include

# define lib directory
LIB		:= ../../Bin

CUR_DIR_NAME := $(notdir $(shell pwd))
OBJ 	:= ../../Obj/Public/$(CUR_DIR_NAME)
MAIN	:= lib$(CUR_DIR_NAME).so

SOURCEDIRS	:= $(shell find $(SRC) -type d)
LIBDIRS		:= $(shell find $(LIB) -name "*.so")
FIXPATH = $1
RM = rm -f
MD	:= mkdir -p

# define any directories containing header files other than /usr/include
INCLUDES	:= $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))

# define the C source files
SOURCES		:= $(wildcard $(patsubst %, %/*.c, $(SOURCEDIRS)))

# define the C object files 
OBJECTS		:= $(patsubst $(SRC)/%, $(OBJ)/%, $(SOURCES:.c=.o))

# define the C libs
LIBS		:= 

#
# The following part of the makefile is generic; it can be used to 
# build any executable just by changing the definitions above and by
# deleting dependencies appended to the file from 'make depend'
#
OUTPUTMAIN	:= $(call FIXPATH,$(OUTPUT)/$(MAIN))

# 在make解析規則之前展開執行
OBJ_DIR 	:= $(dir $(OBJECTS))

all: $(OUTPUT) $(OBJ_DIR1) $(MAIN)
	@echo Executing 'all' complete!

$(OUTPUT):
	$(MD) $(OUTPUT)

# 多目標規則,自動遍歷符合規則的所有目標
$(OBJ_DIR):
	$(MD) $@

# 動態庫需要使用-shared
$(MAIN):$(OBJECTS)
	$(CXX) $(CXXFLAGS) $(INCLUDES) -shared -o $(OUTPUTMAIN) $(OBJECTS)  $(LFLAGS)

# 靜態模式規則遍歷所有的.o目標
$(OBJ)/%.o:$(SRC)/%.c
	$(CXX) -c $(CXXFLAGS) $(INCLUDES) $< -o $@

# 偽目標,只能手動呼叫
.PHONY:clean
clean:
	find $(OBJ) -name "*.o" | xargs rm -rf
	rm -rf $(OUTPUTMAIN)

5.2. 編譯當前目錄所有原始檔

遍歷當前目錄包括子目錄所有原始檔並編譯,並將.o檔案生成到Obj目錄,引用編譯.so檔案,將執行檔案生成到Bin目錄。

# define the Cpp compiler to use
CXX = g++

# define any compile-time flags
CXXFLAGS	:= -std=c++17 -Wall -Wextra -g

# define library paths in addition to /usr/lib
#   if I wanted to include libraries not in /usr/lib I'd specify
#   their path using -Lpath, something like:
LFLAGS =

# define output directory
OUTPUT	:= ../Bin

# define source directory
SRC		:= .

# define include directory
INCLUDE	:= ../include

# define lib directory
LIB		:= ../Bin

# 通過shell獲取當前目錄,然後通過nodir獲取當前目錄名
CUR_DIR_NAME := $(notdir $(shell pwd))
OBJ 	:= ../Obj/$(CUR_DIR_NAME)

MAIN	:= main
# 通過shell命令find找到當前目錄中所有目錄包括子目錄
SOURCEDIRS	:= $(shell find $(SRC) -type d)
INCLUDEDIRS	:= $(shell find $(INCLUDE) -type d)
# 找到所有的.so動態庫
LIBDIRS		:= $(shell find $(LIB) -name "*.so")
FIXPATH = $1
RM = rm -f
MD	:= mkdir -p

# define any directories containing header files other than /usr/include
INCLUDES	:= $(patsubst %,-I%, $(INCLUDEDIRS:%/=%))

# 通過patsubst函式獲取所有目錄中的所有.cpp檔案並賦給SOURCES
SOURCES		:= $(wildcard $(patsubst %, %/*.cpp, $(SOURCEDIRS)))

# 遍歷存在的.cpp檔案生成對應的.o檔案
OBJECTS		:= $(patsubst $(SRC)/%, $(OBJ)/%, $(SOURCES:.cpp=.o))

# define the C libs
LIBS		:= $(LIBDIRS)

#
# The following part of the makefile is generic; it can be used to 
# build any executable just by changing the definitions above and by
# deleting dependencies appended to the file from 'make depend'
#
OUTPUTMAIN	:= $(call FIXPATH,$(OUTPUT)/$(MAIN))

# 在make解析規則之前展開執行
OBJ_DIR	:= $(dir $(OBJECTS))

# 頂級目標
all: $(OUTPUT) $(OBJ_DIR) $(MAIN)
	@echo Executing 'all' complete!

# 建立輸出目錄
$(OUTPUT):
	$(MD) $(OUTPUT)

# 多目標規則,自動遍歷符合規則的所有目標目錄
$(OBJ)/%:
	$(MD) $@

# 生成main
$(MAIN): $(OBJECTS) 
	$(CXX) $(CXXFLAGS) $(INCLUDES) -o $(OUTPUTMAIN) $(OBJECTS) $(LFLAGS) $(LIBS)

# 利用靜態模式規則,遍歷所有的目標檔案並呼叫相應的.cpp檔案
$(OBJ)/%.o:$(SRC)/%.cpp
	$(CXX) -c $(CXXFLAGS) $(INCLUDES) $< -o $@

# 偽目標,只能手動呼叫
.PHONY:clean
clean:
	find $(OBJ) -name "*.o" | xargs rm -rf
	rm -rf $(OUTPUTMAIN)

相關文章