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) ```