本文是本人對OpenWrt的Makefile的理解,並非轉載。
OpenWrt是一個典型的嵌入式Linux工程,瞭解OpenWrt的Makefile的工作過程對提高嵌入式Linux工程的開發能力有極其重要意義。
OpenWrt的主Makefile文件只有100行,可以簡單分爲三部分,1~17行爲前導部分,19~31爲首次執行部分,33~101爲再次執行部分。
前導部分
CURDIR爲make默認變量,默認值爲當前目錄。
前導部分主要把變量TOPDIR賦值爲當前目錄,把變量LC_ALL、LANG賦值爲C,並使用變量延伸指示符export,把上述三個變量延伸到下層Makefile。
使用文件使用指示符include引入$(TOPDIR)/include/host.mk。在OpenWrt的主Makefile文件使用了多次include指示符,說明主Makefile文件被拆分成多個文件,被拆分的文件放在不同的目錄。拆分的目的是明確各部分的功能,而且增加其靈活性。
在前導部分比較費解的是使用world目標,在makefile中基本規則爲:
TARGETS : PREREQUISITES
COMMAND
...
即makefile規則由目標、依賴、命令三部分組成,在OpenWrt的主Makefile文件的第一個目標world沒有依賴和命令。它主要起到指示當make命令不帶目標時所要執行的目標,沒有設定依賴和命令部分表明此目標在此後將會有其他依賴關係或命令。world目標的命令需要進一步參考$(TOPDIR)/include/toplevel.mk和主Makefile文件的再次執行部分。
首次執行部分
OPENWRT_BUILD是區分首次執行與再次執行的變量。在首次執行時使用強制賦值指示符override把OPENWRT_BUILD賦值爲1,並使用變量延伸指示符export把OPENWRT_BUILD延伸。在OPENWRT_BUILD使用強制賦值指示符override意味着make命令行可能引入OPENWRT_BUILD參數。
引入$(TOPDIR)/include/debug.mk、$(TOPDIR)/include/depends.mk、$(TOPDIR)/include/toplevel.mk三個文件,由於TOPDIR是固定的,所以三個文件也是固定的。其中$(TOPDIR)/include/toplevel.mk的135行%::有效解釋首次執行時world目標的規則。
再次執行部分
引入rules.mk、$(INCLUDE_DIR)/depends.mk、$(INCLUDE_DIR)/subdir.mk、target/Makefile、package/Makefile、tools/Makefile、toolchain/Makefile七個文件,rules.mk沒有目錄名,即引入與主Makefile文件目錄相同的rules.mk。在rules.mk定義了INCLUDE_DIR爲$(TOPDIR)/include,所以$(INCLUDE_DIR)/depends.mk實際上與首次執行時引入的$(TOPDIR)/include/depends.mk是同一個文件。
四個子目錄下的Makefile實際上是不能獨立執行。主要利用$(INCLUDE_DIR)/subdir.mk動態建立規則,諸如$(toolchain/stamp-install)目標是靠$(INCLUDE_DIR)/subdir.mk的stampfile函數動態建立。在package/Makefile動態建立了$(package/
stamp-prereq)、$(package/ stamp-cleanup)、$(package/ stamp-compile)、$(package/ stamp-install)、$(package/ stamp-rootfs-prepare)目標。
定義一些使用變量命名的目標,其變量的賦值位置在$(INCLUDE_DIR)/subdir.mk的stampfile函數中。目標只有依賴關係,可能說明其工作順序,在$(INCLUDE_DIR)/subdir.mk的stampfile函數中有進一步說明其目標執行的命令,併爲目標建立一個空文件,即使用變量命名的目標爲真實的文件。
定義一些使用固定的目標規則。
其中:clean是清除編譯結果的目標,清除$(BUILD_DIR) $(BIN_DIR) $(BUILD_LOG_DIR)三個目錄的用意是十分明確。暫時不知道爲什麼執行make target/linux/clean。
dirclean是刪除所有編譯過程產生的目錄和文件的目標,執行dirclean目標依賴於clean,因此將執行clean目標所執行的命令,然後刪除$(STAGING_DIR) $(STAGING_DIR_HOST) $(STAGING_DIR_TOOLCHAIN)
$(TOOLCHAIN_DIR) $(BUILD_DIR_HOST) $(BUILD_DIR_TOOLCHAIN)目錄,以及刪除$(TMP_DIR)目錄。上述目錄的變量均在rules.mk定義。好像刪除staging_dir目錄就意味着刪除staging_dir目錄下的所有子目錄,不知道爲什麼要強調刪除$(STAGING_DIR_HOST) $(STAGING_DIR_TOOLCHAIN) $(TOOLCHAIN_DIR)目錄。同樣刪除builde_dir目錄就意味着刪除builde_dir目錄下的所有子目錄,不知道爲什麼要強調刪除$(BUILD_DIR_TOOLCHAIN)目錄。
tmp/.prereq_packages目標是對所需軟件包的預處理。目標依賴於.config,即執行make menuconfig後將會進行一次所需軟件包的預處理。不知什麼原因在編譯前刪除tmp目錄,執行時無法建立tmp/.prereq_packages文件。
prereq應該是預請求目標,在OpenWrt執行Makefile時好像都要先執行prereq目標。
prepare應該是準備目標,是world依賴的一個僞目標。依賴於文件.config和$(tools/stamp-install) $(toolchain/stamp-install)目標。
world就是編譯的目標。依賴於prepare爲目標和前面提到的變量命名目標。採用取消隱含規則方式執行package/index目標。package/index目標在package/Makefile的92行定義。
package/symlinks和package/symlinks-install是更新或安裝軟件包來源的目標,使用$(SCRIPT_DIR)/feeds腳本文件完成。
package/symlinks-clean是清除軟件包來源的目標,也是使用$(SCRIPT_DIR)/feeds腳本文件完成。
最後使用僞目標.PHONY說明clean dirclean prereq prepare world package/symlinks package/symlinks-install package/symlinks-clean屬於僞目標。通過僞目標說明可以知道可以執行的目標。
本篇的主要目的是想通過分析Makefile,瞭解openwrt編譯過程。着重關注以下幾點:
- openwrt目錄結構
- 主Makefile的解析過程,各子目錄的目標生成。
- kernel編譯過程
- firmware的生成過程
- 軟件包的編譯過程
openwrt目錄結構
官方源下載速度太慢,我從github上clone了openwrt的代碼倉庫。
git clone https://github.com/openwrt-mirror/openwrt.git
上圖是openwrt目錄結構,其中第一行是原始目錄,第二行是編譯過程中生成的目錄。各目錄的作用是:
tools - 編譯時需要一些工具, tools裏包含了獲取和編譯這些工具的命令。裏面是一些Makefile,有的可能還有patch。每個Makefile裏都有一句:$(eval $(call HostBuild)),表示編譯這個工具是爲了在主機上使用的。
toolchain - 包含一些命令去獲取kernel headers, C library, bin-utils, compiler, debugger
target - 各平臺在這個目錄裏定義了firmware和kernel的編譯過程。
package - 包含針對各個軟件包的Makefile。openwrt定義了一套Makefile模板,各軟件包參照這個模板定義了自己的信息,如軟件包的版本、下載地址、編譯方式、安裝地址等。
include - openwrt的Makefile都存放在這裏。
scripts - 一些perl腳本,用於軟件包管理。
dl - 軟件包下載後都放到這個目錄裏
build_dir - 軟件包都解壓到build_dir/裏,然後在此編譯
staging_dir - 最終安裝目錄。tools, toolchain被安裝到這裏,rootfs也會放到這裏。
feeds -
bin - 編譯完成之後,firmware和各ipk會放到此目錄下。
main Makefile
openwrt根目錄下的Makefile是執行make命令時的入口。從這裏開始分析。
world:
ifndef ($(OPENWRT_BUILD),1)
# 第一個邏輯
...
else
# 第二個邏輯
...
endif
上面這段是主Makefile的結構,可以得知:
- 執行make時,若無任何目標指定,則默認目標是world
- 執行make時,無參數指定,則會進入第一個邏輯。如果執行命令
make OPENWRT_BUILD=1
,則直接進入第二個邏輯。
編譯時一般直接使用make
V=s -j 5
這樣的命令,不會指定OPENWRT_BUILD變量,顯然這裏OPENWRT_BUILD用於標記當前openwrt BSP是否已經編譯過,因此第一次編譯當然應該執行第一個邏輯,同時令OPENWRT_BUILD爲1,標示openwrt BSP已經編譯過了。
第一個邏輯
override OPENWRT_BUILD=1
export OPENWRT_BUILD
更改了OPENWRT_BUILD變量的值。這裏起到的作用是下次執行make時,因爲已經編譯過,會進入到第二邏輯中。
第一個邏輯中會include toplevel.mk,該文件中的 “%::” 用於解釋world目標的規則:
prereq:: prepare-tmpinfo .config
@+$(MAKE) -r -s tmp/.prereq-build $(PREP_MK)
@+$(NO_TRACE_MAKE) -r -s $@
%::
@+$(PREP_MK) $(NO_TRACE_MAKE) -r -s prereq
@( \
cp .config tmp/.config; \
./scripts/config/conf --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 $@
執行 make
V=s
時,上面這段規則簡化爲:
prereq:: prepare-tmpinfo .config
@make -r -s tmp/.prereq-build
@make V=ss -r -s prereq
%::
@make V=s -r -s prereq
@make -w -r world
可見其中最終又執行了prereq和world目標,這兩個目標都會進入到第二邏輯中。
第二邏輯
首先就引入了target, package, tools, toolchain這四個關鍵目錄裏的Makefile文件
include target/Makefile
include package/Makefile
include tools/Makefile
include toolchain/Makefile
這些子目錄裏的Makefile使用include/subdir.mk裏定義的兩個函數來動態生成規則,這兩個函數是subdir和stampfile
stampfile
拿target/Makefile舉例:
(eval(call stampfile,$(curdir),target,prereq,.config))
會生成規則:
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 $$(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)
所以可以簡單的看作:(eval(call stampfile,(curdir),target,prereq,.config))生成了目標(target/stamp-prereq)
- 對於target分別生成了:(target/stamp-preq),(target/stamp-copile),
$(target/stamp-install)
- toolchain : $(toolchain/stamp-install)
- package : (package/stamp-preq),(package/stamp-cleanup), (package/stamp-compile),(package/stamp-install)
- tools : $(tools/stamp-install)
subdir
subdir這個函數寫了一大堆東西,看起來很複雜 。
$(call subdir, target) 會遍歷下的子目錄,執行 make -C 操作。這樣就切入子目錄中去了。
目錄變量
幾個重要的目錄路徑:
-
KERNEL_BUILD_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/linux-3.14.18
-
LINUX_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/linux-3.14.18
-
KDIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a
-
BIN_DIR
bin/ramips
Makefile中包含了rules.mk, target.mk等.mk文件,這些文件中定義了許多變量,有些是路徑相關的,有些是軟件相關的。這些變量在整個Makefile工程中經常被用到, -
TARGET_ROOTFS_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2
-
BUILD_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2
-
STAGING_DIR_HOST
staging_dir/toolchain-mipsel_24kec+dsp_gcc-4.8-linaro_uClibc-0.9.33.2
-
TARGET_DIR
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/root-ramips
kernel 編譯:
target/linux/ramips/Makefile: $(eval
$(call BuildTarget))
target/linux/Makefile : export
TARGET_BUILD=1
include/target.mk中如下內容:
ifeq ($(TARGET_BUILD),1)
include $(INCLUDE_DIR)/kernel-build.mk
BuildTarget?=$(BuildKernel)
endif
BuildKernel是include/kernel-build.mk定義的一個多行變量,其中描述瞭如何編譯內核, 主要關注其中install規則的依賴鏈:
$(KERNEL_BUILD_DIR)/symtab.h: FORCE
rm -f $(KERNEL_BUILD_DIR)/symtab.h
touch $(KERNEL_BUILD_DIR)/symtab.h
+$(MAKE) $(KERNEL_MAKEOPTS) 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: install --> $(LINUX_DIR)/.image --> $(KERNEL_BUILD_DIR)/symtab.h --> `$(MAKE) $(KERNEL_MAKEOPTS) vmlinux`
2. 對vmlinux做objcopy, strip操作: $(LINUX_DIR)/.image --> $(Kernel/CompileImage) --> $(call Kernel/CompileImage/Default) --> $(call Kernel/CompileImage/Default)
$(KERNEL_CROSS)objcopy -O binary $(OBJCOPY_STRIP) -S $(LINUX_DIR)/vmlinux $(LINUX_KERNEL)$(1)
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux
$(KERNEL_CROSS)objcopy $(OBJCOPY_STRIP) -S $(LINUX_DIR)/vmlinux $(KERNEL_BUILD_DIR)/vmlinux$(1).elf
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux.elf
$(CP) $(LINUX_DIR)/vmlinux $(KERNEL_BUILD_DIR)/vmlinux.debug
--> build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/vmlinux.debug
生成firmware
firmware由kernel和rootfs兩個部分組成,要對兩個部分先分別處理,然後再合併成一個.bin文件。先看一下這個流程。
"target/linux/ramips/image/Makefile" 文件中的最後一句:$(eval
$(call BuildImage))
,將BuildImage展開在這裏。BuildImage定義在 include/image.mk 文件中,其中定義了數個目標的規則。
define BuildImage
compile: compile-targets FORCE
**$(call Build/Compile)**
install: compile install-targets FORCE
...
$(call Image/BuildKernel) ## 處理vmlinux
...
$(call Image/mkfs/squashfs) ## 生成squashfs,並與vmlinux合併成一個.bin文件
...
endef
處理vmlinux: Image/BuildKernel
target/linux/ramips/image/Makefile:
define Image/BuildKernel
cp $(KDIR)/vmlinux.elf $(BIN_DIR)/$(VMLINUX).elf
cp $(KDIR)/vmlinux $(BIN_DIR)/$(VMLINUX).bin
$(call CompressLzma,$(KDIR)/vmlinux,$(KDIR)/vmlinux.bin.lzma)
$(call MkImage,lzma,$(KDIR)/vmlinux.bin.lzma,$(KDIR)/uImage.lzma)
cp $(KDIR)/uImage.lzma $(BIN_DIR)/$(UIMAGE).bin
ifneq ($(CONFIG_TARGET_ROOTFS_INITRAMFS),)
cp $(KDIR)/vmlinux-initramfs.elf $(BIN_DIR)/$(VMLINUX)-initramfs.elf
cp $(KDIR)/vmlinux-initramfs $(BIN_DIR)/$(VMLINUX)-initramfs.bin
$(call CompressLzma,$(KDIR)/vmlinux-initramfs,$(KDIR)/vmlinux-initramfs.bin.lzma)
$(call MkImage,lzma,$(KDIR)/vmlinux-initramfs.bin.lzma,$(KDIR)/uImage-initramfs.lzma)
cp $(KDIR)/uImage-initramfs.lzma $(BIN_DIR)/$(UIMAGE)-initramfs.bin
endif
$(call Image/Build/Initramfs)
endef
lzma壓縮內核
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/ 目錄中:
lzma e vmlinux -lc1 -lp2 -pb2 vmlinux.bin.lzma
MkImage
build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/ 目錄中:
mkimage -A mips -O linux -T kernel -C lzma -a 0x80000000 -e 0x80000000 -n "MIPS OpenWrt Linux-3.14.18" -d vmlinux.bin.lzma uImage.lzma
copy
VMLINUX:=$(IMG_PREFIX)-vmlinux --> openwrt-ramips-mt7620a-vmlinux
UIMAGE:=$(IMG_PREFIX)-uImage --> openwrt-ramips-mt7620a-uImage
cp $(KDIR)/uImage.lzma $(BIN_DIR)/$(UIMAGE).bin
把uImage.lzma複製到bin/ramips/目錄下:
cp $(KDIR)/uImage.lzma bin/ramips/openwrt-ramips-mt7620a-uImage
製作squashfs,生成.bin: $(call Image/mkfs/squashfs)
define Image/mkfs/squashfs
@mkdir -p $(TARGET_DIR)/overlay
$(STAGING_DIR_HOST)/bin/mksquashfs4 $(TARGET_DIR) $(KDIR)/root.squashfs -nopad -noappend -root-owned -comp $(SQUASHFSCOMP) $(SQUASHFSOPT) -processors $(if $(CONFIG_PKG_BUILD_JOBS),$(CONFIG_PKG_BUILD_JOBS),1)
$(call Image/Build,squashfs)
endif
mkdir -p $(TARGET_DIR)/overlay
mkdir -p build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/root-ramips/overlay
mksquashfs4
$(STAGING_DIR_HOST)/bin/mksquashfs4 $(TARGET_DIR) $(KDIR)/root.squashfs -nopad -noappend -root-owned -comp $(SQUASHFSCOMP) $(SQUASHFSOPT) -processors $(if $(CONFIG_PKG_BUILD_JOBS),$(CONFIG_PKG_BUILD_JOBS),1)
製作squashfs文件系統,生成root.squashfs:
mksquashfs4 root-ramips root.squashfs -nopad -noappend -root-owned -comp gzip -b 256k -p '/dev d 755 0 0' -p '/dev/console c 600 0 0 5 1' -processors 1
$(call Image/Build,squashfs)
在 target/linux/ramips/image/Makefile 中:
define Image/Build
$(call Image/Build/$(1))
dd if=$(KDIR)/root.$(1) of=$(BIN_DIR)/$(IMG_PREFIX)-root.$(1) bs=128k conv=sync
$(call Image/Build/Profile/$(PROFILE),$(1))
endef
- dd if=(KDIR)/root.squashfsof=(BIN_DIR)/$(IMG_PREFIX)-root.squashfs bs=128k conv=sync
dd if=build_dir/target-mipsel_24kec+dsp_uClibc-0.9.33.2/linux-ramips_mt7620a/root.squashfs of=bin/ramips/openwrt-ramips-mt7620-root.squashfs bs=128k conv=sync
- (callImage/Build/Profile/(PROFILE),squashfs)
target/linux/ramips/mt7620a/profiles/00-default.mk, 中調用 Profile 函數:$(eval
$(call Profile,Default))
include/target.mk 中定義了 Profile 函數, 其中令 PROFILE=Default
define Image/Build/Profile/Default
$(call Image/Build/Profile/MT7620a,$(1))
...
endef
規則依賴序列如下:
$(call Image/Build/Profile/$(PROFILE),squashfs)
--> $(call BuildFirmware/Default8M/squashfs,squashfs,mt7620a,MT7620a)
--> $(call BuildFirmware/OF,squashfs,mt7620a,MT7620a,8060928)
--> $(call MkImageLzmaDtb,mt7620a,MT7620a)
--> $(call PatchKernelLzmaDtb,mt7620a,MT7620a)
--> $(call MkImage,lzma,$(KDIR)/vmlinux-mt7620a.bin.lzma,$(KDIR)/vmlinux-mt7620a.uImage)
--> $(call MkImageSysupgrade/squashfs,squashfs,mt7620a,8060928)
其中的主要步驟:
- 複製: cp (KDIR)/vmlinux(KDIR)/vmlinux-mt7620a
- 生成dtb文件:(LINUXDIR)/scripts/dtc/dtc-Odtb-o(KDIR)/MT7620a.dtb ../dts/MT7620a.dts
- 將內核與dtb文件合併:(STAGINGDIRHOST)/bin/patch-dtb(KDIR)/vmlinux-mt7620a
$(KDIR)/MT
- 使用lzma壓縮:(callCompressLzma,(KDIR)/vmlinux-mt7620a,$(KDIR)/vmlinux-mt7620a.bin.lzma)
- 將lzma壓縮後的文件經過mkimage工具處理,即在頭部添加uboot可識別的信息。
接下來就是合併生成firmware固件了:
MkImageSysupgrade/squashfs, squashfs, mt7620a,8060928
cat vmlinux-mt7620a.uImage root.squashfs > openwrt-ramips-mt7620-mt7620a-squashfs-sysupgrade.bin
--> 製作squashfs bin文檔, 並確認它的大小 < 8060928 纔是有效的,否則報錯。
總結: 整個流程下來,其實最煩索的還是對內核生成文件vmlinux的操作,經過了objcopy, patch-dtb, lzma, mkimage 等過程生成一個uImage,再與mksquashfs工具製作的文件系統rootfs.squashfs合併。