OpenWRT Makefile
===
OpenWRT 經由GNU Make導引並建置客製化系統。為了方便開發者開發相容於OpenWRT的套件,建立了以物件導向方式建立Package Makefile的方法。關於Package的詳細介紹會放到Package篇介紹。本篇文章主要介紹OpenWRT主程式編譯的Makefile。
[參考openwrt: Makefile 框架分析](https://blog.csdn.net/JK198310/article/details/102524803)
---
## Makefile簡介
GNU Make透過讀取Makefile腳本,自動建置系統。若看到專案目錄下有Makefile腳本,可以直接使用make指令建置專案。
```bash=
make
```
:::info
使用Makefile的好處是甚麼?GNU Make會自動偵測日期,編譯有變動的原始檔案,並不會所有檔案都編譯,節省編譯時間。
:::
至於Makefile 要怎麼撰寫呢?本章節會透過範例介紹一些常用的方法如果要了解所有的Makefile語法,可以參考[gnu官方文件](https://www.gnu.org/software/make/manual/make.html#Overview)。。
---
## Makefile基本語法
為了瞭解Makefile的用法,我們從最簡單的範例開始了解。[Simple-Makefile](https://www.gnu.org/software/make/manual/make.html#Simple-Makefile)
```make=
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
main.o : main.c defs.h
cc -c main.c
kbd.o : kbd.c defs.h command.h
cc -c kbd.c
command.o : command.c defs.h command.h
cc -c command.c
display.o : display.c defs.h buffer.h
cc -c display.c
insert.o : insert.c defs.h buffer.h
cc -c insert.c
search.o : search.c defs.h buffer.h
cc -c search.c
files.o : files.c defs.h buffer.h command.h
cc -c files.c
utils.o : utils.c defs.h
cc -c utils.c
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
```
上述範例中,edit執行檔編譯依賴著8個目的檔(main.o kbd.o command.o display.o insert.o search.o files.o utils.o)。若是因為一行字數太長,可使用反斜線和換行,這點跟C語言一樣。
```make=
edit : main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
cc -o edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
```
冒號(:)左邊為目標(規則),右邊為來源(檔案或目標,獲取目標所需要的依賴)。edit依賴著main.o kbd.o command.o display.o insert.o search.o files.o utils.o,這幾個檔案。因此GNU Make會從檔案找尋其他目標,將這些生成檔案。
透過make <目標>,可以執行範例檔案中的任一目標。像是
```bash=
make clean
```
可以執行
```make=
clean :
rm edit main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
```
若直接執行make,則GNU make會選擇第一個目標執行。
```bash=
make
```
上述範例中,第一個目標為edit。
身為一個腳本語言,makefile理所當然的擁有變數,迴圈,分支,函式等等語法。之後章節將一一介紹。
---
## Makefile 變數
Makefile中的變數(又稱作巨集)的值是一串字串。這些值被目標,先決條件和Makefile的其他部分等等所取代。
[參考Makefile學習之路(2) — Makefile的變數和萬用字元](https://iter01.com/575781.html)
* 變數宣告
```make=
MACRO = value
```
1. VAR_NAME=VAR_VALUE
延時賦值: Makefile展開後才決定變數的值。
```make=
VAR1=$(VAR2)
VAR2=5
all:
@echo $(VAR1)
```
---
2. VAR_NAME:=VAR_VALUE
立即賦值: Makefile展開後才決定變數的值。
```make=
VAR1:=10
VAR2:=12
all:
@echo $(VAR1)
@echo $(VAR2)
```
---
3. VAR_NAME?=VAR_VALUE
僅有在第一次賦值有效。
```make=
VAR1?=10
VAR1?=12
all:
@echo $(VAR1)
```
---
4. VAR_NAME+=VAR_VALUE
變數附加
```make=
VAR1:=B
VAR1+=C
all:
@echo $(VAR1)
```
* output
```bash=
B C
```
---
## Makefile 函數
Makefile 本身提供了大量函數,可供開發者使用:
```make=
$(function_name argument1, argument2, ...)
```
名稱|解釋
---|---
function_name|函數名稱
argumentO|第O個參數
[Makefile学习之路(3) — Makefile的函数](https://blog.csdn.net/qq_38113006/article/details/111826528)
---
## Makefile 迴圈
Makefile迴圈本身透過for each函數實現。
```make=
$(foreach var,list,text)
```
對於list中每一個用空格出來的值,將這些值賦予var之後,透過text給出的規則生成出新的值,並將生成出的值當作回傳值。
example:
```make=
A = a b c
B = $(foreach var,$(A),$(var).o)
all:
@echo $(B)
```
---
## Makefile 分支
Makefile分支本身透過if函數實現。
```make=
$(if condition,then-part,[,else-part])
```
* 若condition為非空字串,則執行then-part,並將結果當作返回值。
* 若condition為空字串,則執行else-part,並將結果當作返回值。若else-part這時為空,則回傳空值。
```make=
A = 1
B = $(if $(A),@echo "test")
C = $(if $(AA),@echo "1",@echo "2")
all:
$(B)
$(C)
```
---
# OpenWRT Makefile
在了解Makefile基本語法之後,現在開始來分析OpenWRT的Makefile,看看OpenWRT是怎麼Build的。
:::info
因為OpenWRT Makefile編譯過程過於複雜,本篇只記錄較重要的Kernel建置和Firmware建置。
:::
OpenWRT根目錄底下的Makefile是編譯入口,我們從這裡開始分析。[Makefile](https://github.com/openwrt/openwrt/blob/master/Makefile)
```Clike=
world:
ifneq ($(OPENWRT_BUILD),1)
#邏輯1
else
#邏輯2
endif
```
1. 由上述得知,若執行make時,未指定目標,則從world規則開始執行。
2. 若執行make時,無指定參數,則執行邏輯1。
3. 若執行make OPENWRT_BUILD=1,則進入邏輯2。
### 邏輯1
```make=
override OPENWRT_BUILD=1
export OPENWRT_BUILD
```
強制指定OPENWRT_BUILD=1,確保下次執行make時一定會進入邏輯2。
world 目標中指定引入toplevel.mk
```make=
include $(TOPDIR)/include/toplevel.mk
```
toplevel.mk之中的%:: 解釋了world目標規則:
```make=
prereq:: prepare-tmpinfo .config
@+$(NO_TRACE_MAKE) -r -s $@
%::
@+$(PREP_MK) $(NO_TRACE_MAKE) -r -s prereq
@( \
cp .config tmp/.config; \
./scripts/config/conf $(KCONF_FLAGS) --defconfig=tmp/.config -w tmp/.config Config.in > /dev/null 2>&1; \
if ./scripts/kconfig.pl '>' .config tmp/.config | grep -q CONFIG; then \
printf "$(_R)WARNING: your configuration is out of sync. Please run make menuconfig, oldconfig or defconfig!$(_N)\n" >&2; \
fi \
)
@+$(ULIMIT_FIX) $(SUBMAKE) -r $@ $(if $(WARN_PARALLEL_ERROR), || { \
printf "$(_R)Build failed - please re-run with -j1 to see the real error message$(_N)\n" >&2; \
false; \
} )
```
:::info
%:: 匹配所有沒有指定目標
:::
當執行Make V=s時,可簡化成:
```make=
prereq:: prepare-tmpinfo .config
@make V=ss -r -s prereq
%::
@make V=s -r -s prereq
@make -w -r world
```
最終強制在執行一次目標world,由於強制指定OPENWRT_BUILD=1,所以會進入邏輯2。
### 邏輯2
```make=
include rules.mk
include $(INCLUDE_DIR)/depends.mk
include $(INCLUDE_DIR)/subdir.mk
include target/Makefile
include package/Makefile
include tools/Makefile
include toolchain/Makefile
```
subdir.mk定義了兩個動態生成規則的函式 -- subdir(遞迴所有子目錄編譯), stampfile(比對所有時間戳,確認是否需要使用函式subdir編譯)。
#### 其中stampfile:
```make=
define stampfile
$(1)/stamp-$(3):=$(if $(6),$(6),$(STAGING_DIR))/stamp/.$(2)_$(3)$(5)
$$($(1)/stamp-$(3)): $(TMP_DIR)/.build $(4)
@+$(SCRIPT_DIR)/timestamp.pl -n $$($(1)/stamp-$(3)) $(1) $(4) || \
$(MAKE) $(if $(QUIET),--no-print-directory) $$($(1)/flags-$(3)) $(1)/$(3)
@mkdir -p $$$$(dirname $$($(1)/stamp-$(3)))
@touch $$($(1)/stamp-$(3))
$$(if $(call debug,$(1),v),,.SILENT: $$($(1)/stamp-$(3)))
.PRECIOUS: $$($(1)/stamp-$(3)) # work around a make bug
$(1)//clean:=$(1)/stamp-$(3)/clean
$(1)/stamp-$(3)/clean: FORCE
@rm -f $$($(1)/stamp-$(3))
endef
```
如果有個呼叫如下(target/makefile當作範例[連結](https://github.com/openwrt/openwrt/blob/340c2ed2ef6578483f974e274bf6d638f953a246/target/Makefile)):
```make=
curdir:=target
$(eval $(call stampfile,$(curdir),target,prereq,.config))
```
那麼上述就會展開為
```make=
target/stamp-prereq:=$(STAGING_DIR)/stamp/.target_prereq
$$(target/stamp-prereq): $(TMP_DIR)/.build .config
@+$(SCRIPT_DIR)/timestamp.pl -n $$(target/stamp-prereq) target .config || \
$(MAKE) $(if $(QUIET),--no-print-directory) $$(target/flags-prereq) target/prereq
@mkdir -p $$$$(dirname $$(target/stamp-prereq))
@touch $$(target/stamp-prereq)
$$(if $(call debug,target,v),,.SILENT: $$(target/stamp-prereq))
.PRECIOUS: $$(target/stamp-prereq) # work around a make bug
target//clean:=target/stamp-prereq/clean
target/stamp-prereq/clean: FORCE
@rm -f $$(target/stamp-prereq)
```
:::info
```make=
$(if $(6),$(6),$(STAGING_DIR))
```
因為`$(6)`為空,所以回傳$(STAGING_DIR)
:::
#### subdir函數如下:
```make=
# Parameters: <subdir>
define subdir
$(call warn,$(1),d,D $(1))
$(foreach bd,$($(1)/builddirs),
$(call warn,$(1),d,BD $(1)/$(bd))
$(foreach target,$(SUBTARGETS) $($(1)/subtargets),
$(foreach btype,$(buildtypes-$(bd)),
$(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(btype)/$(target): $(if $(NO_DEPS)$(QUILT),,$($(1)/$(bd)/$(btype)/$(target)) $(call $(1)//$(btype)/$(target),$(1)/$(bd)/$(btype))))
$(call log_make,$(1)/$(bd),$(target),$(btype),$(filter-out __default,$(variant)),$($(1)/$(bd)/variants)) \
|| $(call ERROR,$(2), ERROR: $(1)/$(bd) [$(btype)] failed to build.,$(findstring $(bd),$($(1)/builddirs-ignore-$(btype)-$(target))))
$(if $(call diralias,$(bd)),$(call warn_eval,$(1)/$(bd),l,T,$(1)/$(call diralias,$(bd))/$(btype)/$(target): $(1)/$(bd)/$(btype)/$(target)))
)
$(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(target): $(if $(NO_DEPS)$(QUILT),,$($(1)/$(bd)/$(target)) $(call $(1)//$(target),$(1)/$(bd))))
$(foreach variant,$(filter-out *,$(if $(BUILD_VARIANT),$(BUILD_VARIANT),$(if $(strip $($(1)/$(bd)/variants)),$($(1)/$(bd)/variants),$(if $($(1)/$(bd)/default-variant),$($(1)/$(bd)/default-variant),__default)))),
$(if $(BUILD_LOG),@mkdir -p $(BUILD_LOG_DIR)/$(1)/$(bd)/$(filter-out __default,$(variant)))
$(if $($(1)/autoremove),$(call rebuild_check,$(1)/$(bd),$(target),,$(filter-out __default,$(variant)),$($(1)/$(bd)/variants)))
$(call log_make,$(1)/$(bd),$(target),,$(filter-out __default,$(variant)),$($(1)/$(bd)/variants)) \
|| $(call ERROR,$(1), ERROR: $(1)/$(bd) failed to build$(if $(filter-out __default,$(variant)), (build variant: $(variant))).,$(findstring $(bd),$($(1)/builddirs-ignore-$(target))))
)
$(if $(PREREQ_ONLY)$(DUMP_TARGET_DB),,
# aliases
$(if $(call diralias,$(bd)),$(call warn_eval,$(1)/$(bd),l,T,$(1)/$(call diralias,$(bd))/$(target): $(1)/$(bd)/$(target)))
)
)
)
$(foreach target,$(SUBTARGETS) $($(1)/subtargets),$(call subtarget,$(1),$(target)))
endef
```
如果有個呼叫如下(target/makefile當作範例[連結](https://github.com/openwrt/openwrt/blob/340c2ed2ef6578483f974e274bf6d638f953a246/target/Makefile)):
```make=
$(eval $(call subdir,$(curdir)))
```
將`$(curdir)`代入subdir函數後,subdir會去遍歷`$(curdir)`的子目錄來建置。
---
## Kernel建置
Kernel建置從target/linux/`$(ARCH)`/Makefile開始。`$(ARCH)`是目標架構。檔案裡可以看到所有架構最後都是去呼叫BuildTarget函式。
```make=
$(eval $(call BuildTarget))
```
至於BuildTarget函式實際呼叫了什麼呢?
include/target.mk定義了
```make=
ifeq ($(TARGET_BUILD),1)
include $(INCLUDE_DIR)/kernel-build.mk
BuildTarget?=$(BuildKernel)
endif
```
TARGET_BUILD定義在target/linux/Makefile
```make=
export TARGET_BUILD=1
```
`$(BuildKernel)`呼叫了一系列Kernel建置的程序。
BuildKernel函式定義在include/kernel-build.mk[連結](https://github.com/openwrt/openwrt/blob/master/include/kernel-build.mk):
比較重要的有以下幾個部分
```make=
$(KERNEL_BUILD_DIR)/symtab.h: FORCE
rm -f $(KERNEL_BUILD_DIR)/symtab.h
touch $(KERNEL_BUILD_DIR)/symtab.h
+$(KERNEL_MAKE) vmlinux
...
$(LINUX_DIR)/.image: $(STAMP_CONFIGURED) $(if $(CONFIG_STRIP_KERNEL_EXPORTS),$(KERNEL_BUILD_DIR)/symtab.h) FORCE
$(Kernel/CompileImage)
$(Kernel/CollectDebug)
touch $$@
...
install: $(LINUX_DIR)/.image
+$(MAKE) -C image compile install TARGET_BUILD=
```
1. make vmlinux生成vmlinux(Linux kernel映像檔案):
```
install -> $(LINUX_DIR)/.image -> $(if $(CONFIG_STRIP_KERNEL_EXPORTS),$(KERNEL_BUILD_DIR)/symtab.h) -> $(KERNEL_BUILD_DIR)/symtab.h -> $(KERNEL_MAKE) vmlinux -> $(MAKE) $(KERNEL_MAKEOPS) vmlinux
```
2. 將vmlinux封裝成(objcopy, strip)image:
```
$(LINUX_DIR)/.image -> $(Kernel/CompileImage) -> $(Kernel/CompileImage/Default)
```
---
### 生成Firmware
[參考](https://www.itread01.com/content/1546516086.html)
Firmware由kernel和rootfs兩部分組成。OpenWRT會對兩者分開處理後,合併成一個bin檔案。
從target/linux/`$(ARCH)`/Makefile的最後一句得知,BuildImage可建置Image。
```make=
$(eval $(call BuildImage))
```
BuildImage位於include/image.mk,並定義了一些規則:
```make=
define BuildImage
...
$(foreach device,$(TARGET_DEVICES),$(call Device,$(device))
install-images: kernel_prepare $(foreach fs,$(filter-out $(if $(UBIFS_OPTS),,ubifs),$(TARGET_FILESYSTEMS) $(fs-subtypes-y)),$(KDIR)/root.$(fs))
$(foreach fs,$(TARGET_FILESYSTEMS),
$(call Image/Build,$(fs))
)
...
endef
```
依據規則,我們會先執行到
```make=
$(foreach device,$(TARGET_DEVICES),$(call Device,$(device))
```
函數Device定義在include/image.mk:
```make=
define Device
$(call Device/InitProfile,$(1))
$(call Device/Init,$(1))
$(call Device/Default,$(1))
$(call Device/$(1),$(1))
$(call Device/Check,$(1))
$(call Device/$(if $(DUMP),Dump,Build),$(1))
endef
```
這行定義了如何建置firmware。
```make=
$(call Device/$(if $(DUMP),Dump,Build),$(1))
```
因為DUMP為空,所以可以展開成:
```make=
$(call Device/Build,$(1))
```
呼叫定義在同一個檔案的函式Device/Build
```make=
define Device/Build
$(if $(CONFIG_TARGET_ROOTFS_INITRAMFS),$(call Device/Build/initramfs,$(1)))
$(call Device/Build/kernel,$(1))
$$(eval $$(foreach compile,$$(COMPILE), \
$$(call Device/Build/compile,$$(compile),$(1))))
$$(eval $$(foreach image,$$(IMAGES), \
$$(foreach fs,$$(filter $(TARGET_FILESYSTEMS),$$(FILESYSTEMS)), \
$$(call Device/Build/image,$$(fs),$$(image),$(1)))))
$$(eval $$(foreach artifact,$$(ARTIFACTS), \
$$(call Device/Build/artifact,$$(artifact),$(1))))
endef
```
執行流程
```
Device/Build/kernel (建置Firmware核心) -> Device/Build/compile (編譯) -> Device/Build/image (建置映像檔案) -> Device/Build/artifact(建置bin file)
```