Linux開發之Makefile簡明教程及示例
前言
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)
相關文章
- makefile 簡明教程
- [zz]makefile寫法簡單示例
- 羽夏 MakeFile 簡明教程
- 《簡明 PHP 教程》00 開篇PHP
- Redux 莞式教程 之 簡明篇Redux
- Android 極簡反射教程及應用示例Android反射
- API介面開發簡述示例API
- 簡明 docker 教程Docker
- IText簡介及示例
- 簡明 Python 教程Python
- JavaScript開發工具簡明歷史JavaScript
- Makefile使用教程1
- GitBook簡明安裝教程Git
- 簡明 MongoDB 入門教程MongoDB
- 簡明Python 教程 --模組Python
- 很詳細、很移動的Linux makefile 教程Linux
- makefile 簡明教程 —— 助你更好理解 C 專案組織
- C#開發Unity遊戲教程迴圈遍歷做出判斷及Unity遊戲示例C#Unity遊戲
- Onvif開發之Linux下gsoap的使用及移植Linux
- 菜鳥入門:Linux之Makefile概述(轉)Linux
- 最簡明的Shiro教程
- 《簡明 PHP 教程》04 基礎PHP
- 《簡明 PHP 教程》02 安裝PHP
- Raspberry Pi 3簡明配置教程
- iOS Core Animation 簡明系列教程iOS
- Linux發展史及簡介Linux
- 【GLSL教程】(四)shder的簡單示例
- .net持續整合cake篇之cake介紹及簡單示例
- WebGL簡易教程(一):第一個簡單示例Web
- iOS開發之Runtime常用示例總結iOS
- linux下安裝protobuf教程+示例Linux
- makefile之overrideIDE
- Android開發簡單教程.docAndroid
- iOS Touch ID 簡易開發教程iOS
- 《簡明 PHP 教程》01 關於 PHPPHP
- 哪有簡明python教程下載?Python
- NFT元宇宙鏈遊系統開發說明(原始碼示例)元宇宙原始碼
- linux下使用vscode和makefile搭建C++開發環境LinuxVSCodeC++開發環境