openwrt 中的 include/scan.mk 用于扫描项目 package, target 目录信息,并将扫描结果存入 tmp 目录。这个扫描过程几乎是 openwrt 所有目标生成的前提。也就是说,无论使用 make 编译 openwrt 哪个部分的代码,都会通过 scan.mk 生成必要的临时文件,这是编译其它目录的大前提。
举例说明,我们指定编译某个 package 时,如 package/utils/demo,make 根据层层 Makefile 会去寻找该 package 的路径,而这个路径信息就是通过 scan.mk 扫描后存入了 tmp 目录。这样有什么好处呢? 我完全可以手动执行 make package/utils/demo/compile 不是吗?
的确如此,但是我们不可能每次都去写长串的路径,通过 tmp 目录的信息,不管 package 对应目录在哪, package/demo 也好, package/utils/demo 也罢, package/utils/test/demo 也无所谓,我们都可以执行 make package/demo/compile 进行编译,make 会根据 tmp 目录里保存的映射关系自动查找到对应目录,非常方便。
openwrt 的 Makefile 体系非常庞大,通过首次生成 package、target 信息到 tmp 目录,可以简化编译流程,节省编译时间。这篇文章就来详细讲述一下 scan.mk 的扫描过程。
prepare-tmpinfo 在讲述 scan.mk 之前,我们需要知道 scan.mk 在哪里被调用到,答案是 toplevel.mk 的 prepare-tmpinfo 目标,这个目标几乎是 toplevel.mk 中其它目标都会包含的依赖项。defconfig, oldconfig, menuconfig, prereq, config 等都会依赖它。
顾名思义,prepare-tmpinfo 就是用来准备 tmp 信息的,它没有依赖项,FORCE 代表强制执行其指令。在它的指令中就会调用到 scan.mk 了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 prepare-tmpinfo: FORCE @+$(MAKE) -r -s staging_dir/host/.prereq-build $(PREP_MK) mkdir -p tmp/info $(_SINGLE) $(NO_TRACE_MAKE) -j1 -r -s -f include /scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package" SCAN_NAME="package" SCAN_DEPTH=5 SCAN_EXTRA="" $(_SINGLE) $(NO_TRACE_MAKE) -j1 -r -s -f include /scan.mk SCAN_TARGET="targetinfo" SCAN_DIR="target/linux" SCAN_NAME="target" SCAN_DEPTH=2 SCAN_EXTRA="" SCAN_MAKEOPTS="TARGET_BUILD=1" for type in package target; do \ f=tmp/.$${type}info; t=tmp/.config-$${type}.in; \ [ "$$t" -nt "$$f" ] || ./scripts/$${type}-metadata.pl $(_ignore) config "$$f" > "$$t" || { rm -f "$$t" ; echo "Failed to build $$t" ; false; break; }; \ done [ tmp/.config-feeds.in -nt tmp/.packageauxvars ] || ./scripts/feeds feed_config > tmp/.config-feeds.in ./scripts/package-metadata.pl mk tmp/.packageinfo > tmp/.packagedeps || { rm -f tmp/.packagedeps; false; } ./scripts/package-metadata.pl pkgaux tmp/.packageinfo > tmp/.packageauxvars || { rm -f tmp/.packageauxvars; false; } ./scripts/package-metadata.pl usergroup tmp/.packageinfo > tmp/.packageusergroup || { rm -f tmp/.packageusergroup; false; } touch $(TOPDIR) /tmp/.build
其中有两行调用了 scan.mk.
1 2 $(_SINGLE) $(NO_TRACE_MAKE) -j1 -r -s -f include /scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package" SCAN_NAME="package" SCAN_DEPTH=5 SCAN_EXTRA="" $(_SINGLE) $(NO_TRACE_MAKE) -j1 -r -s -f include /scan.mk SCAN_TARGET="targetinfo" SCAN_DIR="target/linux" SCAN_NAME="target" SCAN_DEPTH=2 SCAN_EXTRA="" SCAN_MAKEOPTS="TARGET_BUILD=1"
通过分析主 Makefile 和 include/verbose.mk 可以知道 $(_SINGLE)$(NO_TRACE_MAKE) 对应的是:
1 export MAKEFLAGS= ;make V=ss
那么以上指令解析后就是:
1 2 export MAKEFLAGS= ;make V=ss -j1 -r -s -f include/scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package" SCAN_NAME="package" SCAN_DEPTH=5 SCAN_EXTRA="" export MAKEFLAGS= ;make V=ss -j1 -r -s -f include/scan.mk SCAN_TARGET="targetinfo" SCAN_DIR="target/linux" SCAN_NAME="target" SCAN_DEPTH=2 SCAN_EXTRA="" SCAN_MAKEOPTS="TARGET_BUILD=1"
说明: make 的 -s 指令代表 silent, 会将所有输出都屏蔽掉,我们在分析的时候可以把 -s 去掉,并换成 -d ,这样可以看到更详细的 log.
好啦,现在知道 scan.mk 的入口啦,就是 prepare-tmpinfo 的指令之一。那么我们怎么触发这两条指令呢?很简单,因为只要执行make就会调用这个依赖,我们可以通过 make defconfig 触发,为了获取更详细的信息,可以使用以下指令:
1 make -d V=s DEBUG=dtlrv defconfig > log 2>&1
这样就将编译信息保存到文件 log 中了,方便分析执行过程。make defconfig 的主要流程在之前已有文章单独讲述过,不再赘述,本文主要来分析
1 make V=ss -j1 -r -s -f include/scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package" SCAN_NAME="package" SCAN_DEPTH=5 SCAN_EXTRA=""
这条指令的执行过程,以深入理解 openwrt scan.mk 的扫描过程。
当然,我们也可以直接调用以上指令,而不用 make defconfig, 只不过需要添加两个全局变量 -- SCAN_COOKIE="123456" -- TOPDIR="/home/litreily/openwrt"
1 make V=ss -j1 -r -d -f include/scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package" SCAN_NAME="package" SCAN_DEPTH=5 SCAN_EXTRA="" SCAN_COOKIE="123456" TOPDIR="/home/litreily/openwrt"
scan.mk 编译过程与使用 make defconfig 是类似的。
scan.mk 进入正题,先附上完整的 scan.mk , 源自 GitHub openwrt.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 include $( TOPDIR )/include /verbose.mkTMP_DIR : =$( TOPDIR )/tmpall: $( TMP_DIR )/.$( SCAN_TARGET )SCAN_TARGET ?= packageinfoSCAN_NAME ?= packageSCAN_DIR ?= packageTARGET_STAMP : =$( TMP_DIR )/info/.files-$( SCAN_TARGET ).stampFILELIST : =$( TMP_DIR )/info/.files-$( SCAN_TARGET )-$( SCAN_COOKIE )OVERRIDELIST : =$( TMP_DIR )/info/.overrides-$( SCAN_TARGET )-$( SCAN_COOKIE ) export PATH : =$( TOPDIR )/staging_dir/host/bin: $( PATH ) define feedname$( if $( patsubst feeds/%,,$( 1 )),,$( word 2 ,$( subst /, ,$( 1 )))) endef ifeq ($( SCAN_NAME ),target) SCAN_DEPS =image/Makefile profiles/*.mk $( TOPDIR )/include /kernel*.mk $( TOPDIR )/include /target.mk image/*.mkelse SCAN_DEPS =$( TOPDIR )/include /package*.mk ifneq ($( call feedname,$( SCAN_DIR )),) SCAN_DEPS += $( TOPDIR )/feeds/$( call feedname,$( SCAN_DIR ))/*.mk endif endif ifeq ($( IS_TTY ),1 ) ifneq ($( strip $( NO_COLOR )),1 ) define progress printf "\033[M\r$(1)" >&2 ; endef else define progress printf "\r$(1)" >&2 ; endef endif else define progress : ; endef endif define PackageDir $( TMP_DIR )/.$( SCAN_TARGET ): $( TMP_DIR )/info/.$( SCAN_TARGET )-$( 1 ) $( TMP_DIR )/info/.$( SCAN_TARGET )-$( 1 ): $( SCAN_DIR )/$( 2 )/Makefile $( foreach DEP ,$( DEPS_ $( SCAN_DIR )/$( 2 )/Makefile ) $( SCAN_DEPS ),$( wildcard $( if $( filter /%,$( DEP )),$( DEP ),$( SCAN_DIR )/$( 2 )/$( DEP )))) { \ $$ (call progress,Collecting $( SCAN_NAME ) info: $( SCAN_DIR )/$( 2 )) \ echo Source -Makefile : $( SCAN_DIR )/$( 2 )/Makefile ; \ $( if $( 3 ),echo Override : $( 3 ),true ); \ $( NO_TRACE_MAKE ) --no-print-dir -r DUMP =1 FEED ="$(call feedname,$(2))" -C $( SCAN_DIR )/$( 2 ) $( SCAN_MAKEOPTS ) 2 >/dev/null | | { \ mkdir -p "$(TOPDIR)/logs/$(SCAN_DIR)/$(2)" ; \ $( NO_TRACE_MAKE ) --no-print-dir -r DUMP =1 FEED ="$(call feedname,$(2))" -C $( SCAN_DIR )/$( 2 ) $( SCAN_MAKEOPTS ) > $( TOPDIR )/logs/$( SCAN_DIR )/$( 2 )/dump.txt 2 >&1 ; \ $$ (call progress,ERROR : please fix $( SCAN_DIR )/$( 2 )/Makefile - see logs/$( SCAN_DIR )/$( 2 )/dump.txt for details\n) \ rm -f $$ @; \ }; \ echo; \ } > $$ @.tmp mv $$ @.tmp $$ @ endef$( OVERRIDELIST ): rm -f $( TMP_DIR )/info/.overrides-$( SCAN_TARGET )-* touch $@ ifeq ($( SCAN_NAME ),target) GREP_STRING =BuildTarget else GREP_STRING =(Build /DefaultTargets |BuildPackage |KernelPackage ) endif$( FILELIST ): $( OVERRIDELIST ) rm -f $( TMP_DIR )/info/.files-$( SCAN_TARGET )-* find -L $( SCAN_DIR ) $( SCAN_EXTRA ) -mindepth 1 $( if $( SCAN_DEPTH ),-maxdepth $( SCAN_DEPTH )) -name Makefile | xargs grep -aHE 'call $(GREP_STRING)' | sed -e 's#^$(SCAN_DIR)/##' -e 's#/Makefile:.*##' | uniq | awk -v of=$( OVERRIDELIST ) -f include /scan.awk > $@ $( TMP_DIR )/info/.files-$( SCAN_TARGET ).mk: $( FILELIST ) ( \ cat $< | awk '{print "$(SCAN_DIR)/" $$0 "/Makefile" }' | xargs grep -HE '^ *SCAN_DEPS *= *' | awk -F: '{ gsub(/^.*DEPS *= */, "", $$2); print "DEPS_" $$1 "=" $$2 }'; \ awk -F/ -v deps="$$DEPS" -v of="$(OVERRIDELIST)" ' \ BEGIN { \ while (getline < (of)) \ override[$$NF]=$$0; \ close(of) \ } \ { \ info=$$0; \ gsub(/\//, "_", info); \ dir=$$0; \ pkg=""; \ if ($$NF in override) \ pkg=override[$$NF]; \ print "$$(eval $$(call PackageDir," info "," dir "," pkg "))"; \ } ' < $<; \ true ; \ ) > $@.tmp mv $@.tmp $@ -include $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk $(TARGET_STAMP):: +( \ $(NO_TRACE_MAKE) $(FILELIST); \ MD5SUM=$$(cat $(FILELIST) $(OVERRIDELIST) | mkhash md5 | awk '{print $$1}'); \ [ -f "$@.$$MD5SUM" ] || { \ rm -f $@.*; \ touch $@.$$MD5SUM; \ touch $@; \ } \ ) $(TMP_DIR)/.$(SCAN_TARGET): $(TARGET_STAMP) $(call progress,Collecting $(SCAN_NAME) info: merging...) -cat $(FILELIST) | awk '{gsub(/\//, "_", $$0);print "$(TMP_DIR)/info/.$(SCAN_TARGET)-" $$0}' | xargs cat > $@ 2>/dev/null $(call progress,Collecting $(SCAN_NAME) info: done) echo FORCE: .PHONY: FORCE .NOTPARALLEL:
global value 在分析全局变量之前,先来看下默认目标 all.
1 all: $(TMP_DIR) /.$(SCAN_TARGET)
$(TMP_DIR) 对应根目录下的 tmp 目录,SCAN_TARGET 在调用 make 的时候有定义,此处为 packageinfo, 因此 all 为:
1 all: /home/litreily/openwrt/tmp/.packageinfo
也就是说,扫描的目标文件是 tmp 目录的 .packageinfo. 但是在生成该目标之前,make 会先 include 其它文件,如果 include 的文件不存在,则会先生成该文件,此处具体指代的是后续讲述的 .files-packageinfo.mk.
ok, 编译目标知道了,再来看看全局变量有哪些。
1 2 3 4 5 6 7 8 9 10 TMP_DIR:=$(TOPDIR) /tmpall: $(TMP_DIR) /.$(SCAN_TARGET) SCAN_TARGET ?= packageinfo SCAN_NAME ?= package SCAN_DIR ?= package TARGET_STAMP:=$(TMP_DIR) /info/.files-$(SCAN_TARGET) .stamp FILELIST:=$(TMP_DIR) /info/.files-$(SCAN_TARGET) -$(SCAN_COOKIE) OVERRIDELIST:=$(TMP_DIR) /info/.overrides-$(SCAN_TARGET) -$(SCAN_COOKIE)
其中 SCAN_COOKIE 是在 toplevel.mk 通过 $(shell echo $$$$) 得到的一个随机数,这里为 2109133. 其它变量可以根据传入的参数解析出来:
1 2 3 4 5 6 7 8 9 10 TMP_DIR:=/home/litreily/openwrt/tmpall: /home/litreily/openwrt/tmp/.packageinfo SCAN_TARGET ?= packageinfo SCAN_NAME ?= package SCAN_DIR ?= package TARGET_STAMP:=/home/litreily/openwrt/tmp/info/.files-packageinfo.stamp FILELIST:=/home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 OVERRIDELIST:=/home/litreily/openwrt/tmp/info/.overrides-packageinfo-2109133
其中,后面三个变量定义的是一些中间目标文件,是生成 all 目标必不可少的中间依赖文件。
.files-packageinfo.mk ok, 全局变量及目标已经确定了,那么 make 执行过程究竟是怎样的呢,在启用调试信息的情况下,可以通过 log 很清晰的看到执行流程。
include include/verbose.mk
include tmp/info/.files-packageinfo.mk
在读取 verbose.mk 后,会根据 scan.mk 执行剩下的 include 指令
1 -include $(TMP_DIR) /info/.files-$(SCAN_TARGET) .mk
导入 tmp/info/.file-packageinfo.mk 文件,该文件默认不存在,所以前面有个 - 符号以确保文件不存在时能够正常执行。
下一步就是将该文件作为目标文件,查找其依赖。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 $(TMP_DIR) /info/.files-$(SCAN_TARGET) .mk: $(FILELIST) ( \ cat $< | awk '{print "$(SCAN_DIR) /" $$0 "/Makefile" }' | xargs grep -HE '^ *SCAN_DEPS *= *' | awk -F: '{ gsub(/^.*DEPS *= */, "" , $$2); print "DEPS_" $$1 "=" $$2 }'; \ awk -F/ -v deps="$$DEPS" -v of="$(OVERRIDELIST) " ' \ BEGIN { \ while (getline < (of)) \ override [$$NF]=$$0; \ close(of) \ } \ { \ info=$$0; \ gsub(/\//, "_" , info); \ dir=$$0; \ pkg="" ; \ if($$NF in override ) \ pkg=override [$$NF]; \ print "$$(eval $$(call PackageDir," info "," dir "," pkg "))" ; \ } ' < $< ; \ true; \ ) > $@ .tmp mv $@ .tmp $@
其依赖是 $(FILELIST), 也就是 tmp/info/.files-packageinfo-2109133. 那么接着来看 $(FILELIST) 的依赖及其指令。
1 2 3 $( FILELIST ): $( OVERRIDELIST ) rm -f $( TMP_DIR )/info/.files-$( SCAN_TARGET )-* find -L $( SCAN_DIR ) $( SCAN_EXTRA ) -mindepth 1 $( if $( SCAN_DEPTH ),-maxdepth $( SCAN_DEPTH )) -name Makefile | xargs grep -aHE 'call $(GREP_STRING)' | sed -e 's#^$(SCAN_DIR)/##' -e 's#/Makefile:.*##' | uniq | awk -v of=$( OVERRIDELIST ) -f include /scan.awk > $@
可知其依赖文件是 $(OVERRIDELIST), 也就是 /home/litreily/openwrt/tmp/info/.overrides-packageinfo-2109133. 而 $(OVERRIDELIST) 规则如下:
1 2 3 $(OVERRIDELIST) : rm -f $(TMP_DIR) /info/.overrides-$(SCAN_TARGET) -* touch $@
该规则很简单,也就是删除旧的 tmp/info/.overrides-packageinfo-* 文件,并 touch 新的文件 tmp/info/.overrides-packageinfo-2109133.
那么执行完以上两条指令后,解析 $(FILELIST) 后的格式为:
1 2 3 /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 : /home/litreily /openwrt/tmp /info/ .overrides-packageinfo-2109133 rm -f /home/litreily/openwrt/tmp/info/.files-packageinfo-* find -L package -mindepth 1 -maxdepth 5 -name Makefile | xargs grep -aHE 'call (Build/DefaultTargets |BuildPackage |KernelPackage)' | sed -e 's#^package/##' -e 's#/Makefile:.*##' | uniq | awk -v of=/home/litreily /openwrt/tmp /info/ .overrides-packageinfo-2109133 -f include /scan.awk > /home/litreily /openwrt/tmp /info/ .files-packageinfo-2109133
与 $(OVERRIDELIST) 类似,先把旧的 tmp/info/.files-packageinfo-* 删除,然后生成新的 tmp/info/.files-packageinfo-2109133. 生成文件用的就是上面的 find 指令了,该指令会查找 package 下 1~5 级目录内的所有 Makefile 文件
1 find -L package -mindepth 1 -maxdepth 5 -name Makefile
然后根据关键词正则过滤包含 call (Build/DefaultTargets|BuildPackage|KernelPackage) 信息的 Makefile, 并通过 uniq 去掉重复项,使用 awk 指令结合 awk 脚本 scan.awk 过滤 feeds 相关的 Makefile, 最终将过滤后的 packageinfo 存入 tmp/info/.files-packageinfo-2109133。
1 2 3 4 5 6 7 8 9 10 base-files boot/arm-trusted-firmware-mvebu boot/arm-trusted-firmware-rockchip boot/arm-trusted-firmware-sunxi boot/at91bootstrap boot/fconfig utils/ugps utils/usbmode utils/util-linux
至此,$(FILELIST) 编译完成,依赖它的目标 tmp/info/.file-packageinfo.mk 可以继续执行对应的指令。把所有变量替换为具体的值,可以得到以下规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 /home/litreily/openwrt/tmp/info/.file-packageinfo.mk: /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 ( \ cat /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 | awk '{print "package/" $0 "/Makefile" }' | xargs grep -HE '^ *SCAN_DEPS *= *' | awk -F: '{ gsub(/^.*DEPS *= */, "" , $2); print "DEPS_" $1 "=" $2 }'; \ awk -F/ -v deps="$DEPS" -v of="/home/litreily/openwrt/tmp/info/.overrides-packageinfo-2109133" ' \ BEGIN { \ while (getline < (of)) \ override [$NF]=$0; \ close(of) \ } \ { \ info=$0; \ gsub(/\//, "_" , info); \ dir=$0; \ pkg="" ; \ if($NF in override ) \ pkg=override [$NF]; \ print "$(eval $(call PackageDir," info "," dir "," pkg "))" ; \ } ' < /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133; \ true; \ ) > /home/litreily/openwrt/tmp/info/.file-packageinfo.mk.tmp mv /home/litreily/openwrt/tmp/info/.file-packageinfo.mk.tmp /home/litreily/openwrt/tmp/info/.file-packageinfo.mk
以上一堆操作的目的都是为了根据前面生成的 $(FILELIST) 去生成 tmp/info/.file-packageinfo.mk.
1 cat /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 | awk '{print "package/" $0 "/Makefile" }' | xargs grep -HE '^ *SCAN_DEPS *= *' | awk -F: '{ gsub(/^.*DEPS *= */, "", $2); print "DEPS_" $1 "=" $2 }' ;
这一段脚本生成了 tmp/info/.file-packageinfo.mk 的前几行信息。
1 2 DEPS_package/firmware/linux-firmware/Makefile=*.mk DEPS_package/kernel/linux/Makefile=modules/*.mk $(TOPDIR) /target/linux/*/modules.mk $(TOPDIR) /include /netfilter.mk
1 2 3 4 5 6 7 8 9 { \ info=$0 ; \ gsub(/\//, "_" , info); \ dir =$0 ; \ pkg="" ; \ if ($NF in override) \ pkg=override[$NF ]; \ print "$(eval $(call PackageDir," info " ," dir " ," pkg " ) )" ; \ } ' < /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133;
以上这段脚本则是根据 package 列表生成 PackageDir 信息列表,以 boot/fconfig 为例。经过以上 awk 变换后变为:
1 $(eval $(call PackageDir,boot_fconfig,boot/fconfig,) )
最终生成的完成的 tmp/info/.files-packageinfo.mk 如下:
1 2 3 4 5 6 7 8 9 10 11 12 DEPS_package/firmware/linux-firmware/Makefile=*.mk DEPS_package/kernel/linux/Makefile=modules/*.mk $(TOPDIR) /target/linux/*/modules.mk $(TOPDIR) /include /netfilter.mk$(eval $(call PackageDir,base-files,base-files,) )$(eval $(call PackageDir,boot_arm-trusted-firmware-mvebu,boot/arm-trusted-firmware-mvebu,) )$(eval $(call PackageDir,boot_arm-trusted-firmware-rockchip,boot/arm-trusted-firmware-rockchip,) )$(eval $(call PackageDir,boot_arm-trusted-firmware-sunxi,boot/arm-trusted-firmware-sunxi,) )$(eval $(call PackageDir,boot_at91bootstrap,boot/at91bootstrap,) )$(eval $(call PackageDir,boot_fconfig,boot/fconfig,) )$(eval $(call PackageDir,utils_ugps,utils/ugps,) )$(eval $(call PackageDir,utils_usbmode,utils/usbmode,) )$(eval $(call PackageDir,utils_util-linux,utils/util-linux,) )
到此,include $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk 就完成了. 该文件中每一项都调用了函数 PackageDir. 该函数是在 scan.mk 中定义的。
PackageDir PackageDir 是 scan.mk 文件中的核心函数之一,用来生成 package, target 相关的编译规则。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 define PackageDir $(TMP_DIR) /.$(SCAN_TARGET) : $(TMP_DIR) /info/.$(SCAN_TARGET) -$(1) $(TMP_DIR) /info/.$(SCAN_TARGET) -$(1): $(SCAN_DIR) /$(2)/Makefile $(foreach DEP,$(DEPS_$(SCAN_DIR) /$(2) /Makefile) $(SCAN_DEPS) ,$(wildcard $(if $(filter /%,$(DEP) ) ,$(DEP) ,$(SCAN_DIR) /$(2)/$(DEP) ))) { \ $$(call progress,Collecting $(SCAN_NAME) info: $(SCAN_DIR) /$(2) ) \ echo Source-Makefile: $(SCAN_DIR) /$(2)/Makefile; \ $(if $(3) ,echo Override: $(3),true); \ $(NO_TRACE_MAKE) --no-print-dir -r DUMP=1 FEED="$(call feedname,$(2))" -C $(SCAN_DIR) /$(2) $(SCAN_MAKEOPTS) 2>/dev/null || { \ mkdir -p "$(TOPDIR) /logs/$(SCAN_DIR) /$(2)" ; \ $(NO_TRACE_MAKE) --no-print-dir -r DUMP=1 FEED="$(call feedname,$(2))" -C $(SCAN_DIR) /$(2) $(SCAN_MAKEOPTS) > $(TOPDIR) /logs/$(SCAN_DIR) /$(2)/dump.txt 2>&1; \ $$(call progress,ERROR: please fix $(SCAN_DIR) /$(2) /Makefile - see logs/$(SCAN_DIR) /$(2)/dump.txt for details\n) \ rm -f $$@ ; \ }; \ echo; \ } > $$@ .tmp mv $$@ .tmp $$@ endef
举例说明,下面的语句中 $(1) 和 $(2) 都是 base-files, $(3) 为空。
1 $(eval $(call PackageDir,base-files,base-files,) )
将变量替换后得到 PackageDir:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 define PackageDir /home/litreily/openwrt/tmp/.packageinfo: /home/litreily/openwrt/tmp/info/.packageinfo-base-files /home/litreily/openwrt/tmp/info/.packageinfo-base-files: package/base-files/Makefile $(foreach DEP,$(DEPS_package/base-files/Makefile) /home/litreily/openwrt/include /package*.mk,$(wildcard $(if $(filter /%,$(DEP) ) ,$(DEP) ,package/base-files/$(DEP) ))) { \ $(call progress,Collecting base-files info: package/base-files) \ echo Source-Makefile: package/base-files/Makefile; \ $(if ,echo Override: ,true) ; \ $(NO_TRACE_MAKE) --no-print-dir -r DUMP=1 FEED="$(call feedname,base-files)" -C package/base-files $(SCAN_MAKEOPTS) 2>/dev/null || { \ mkdir -p "/home/litreily/openwrt/logs/package/base-files" ; \ $(NO_TRACE_MAKE) --no-print-dir -r DUMP=1 FEED="$(call feedname,base-files)" -C package/base-files $(SCAN_MAKEOPTS) > /home/litreily/openwrt/logs/package/base-files/dump.txt 2>&1; \ $(call progress,ERROR: please fix package/base-files/Makefile - see logs/package/base-files/dump.txt for details\n) \ rm -f $@ ; \ }; \ echo; \ } > $@ .tmp mv $@ .tmp $@ endef
注意到这里有定义两个目标。
/home/litreily/openwrt/tmp/.packageinfo
/home/litreily/openwrt/tmp/info/.packageinfo-base-files
注意 : 其中第一个目标正好是 all 目标,并且其依赖是随之其后的 .packageinfo-$(package). 所以目标 all 编译完成的前提之一就是所有 .packageinfo-$(package) 文件的生成。
以 base-files 为例, 通过进一步解析简化,可以得到 tmp/info/.packageinfo-base-files 的规则如下:
1 2 3 4 5 6 7 /home/litreily/openwrt/tmp/info/.packageinfo-base-files: package/base-files/Makefile /home/litreily/openwrt/include/package-*.mk { \ $(call progress,Collecting base-files info: package/base-files) \ echo Source-Makefile: package/base-files/Makefile; \ make V=s --no-print-dir -r DUMP=1 FEED=" -C package/base-files 2>/dev/null \ } > $@ .tmp mv $@ .tmp $@
其中包括打印 Collecting base-files info: package/base-files 这种log,同时会执行 make 子进程
1 make V=s --no-print-dir -r DUMP=1 FEED=" -C package/base-files 2>/dev/null \
将信息写入 tmp/info/.packageinfo-base-files, 也就完成了目标的编译。
这个 make 子进程的重点是 DUMP=1 , package/base-files/Makefile 会根据该变量打印 base-files 相关信息到指定文件。具体要看该Makefile.
针对 base-files, dump 出来的信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 Source-Makefile: package/base-files/Makefile Build-Depends: usign/host ucert/host Package: base-files Version: 246- Depends: +libc +USE_GLIBC:librt +USE_GLIBC:libpthread +netifd +jsonfilter +SIGNED_PACKAGES:usign +SIGNED_PACKAGES:openwrt-keyring +NAND_SUPPORT:ubi-utils +fstools +fwtool Conflicts: Menu-Depends: Provides: Section: base Category: Base system Title: Base filesystem for OpenWrt Maintainer: Source: License: GPL-2.0 Type: ipkg Description: This package contains a base filesystem and system scripts for OpenWrt. http://openwrt.org/ @@
说了这么多,PackageDir 函数何时调用呢?继续往后看。
make all include 相关依赖准备好后,make 开始解析默认目标 all 对应的依赖和指令,也就是 tmp/.packageinfo 目标。
1 2 3 4 5 $(TMP_DIR) /.$(SCAN_TARGET) : $(TARGET_STAMP) $(call progress,Collecting $(SCAN_NAME) info: merging...) -cat $(FILELIST) | awk '{gsub(/\//, "_" , $$0);print "$(TMP_DIR) /info/.$(SCAN_TARGET) -" $$0}' | xargs cat > $@ 2>/dev/null $(call progress,Collecting $(SCAN_NAME) info: done) echo
其依赖为 $(TARGET_STAMP). 当然它的依赖不止这一个,前面 PackageDir 定义的规则中包含的目标也都是它的依赖。下面先来看看 $(TARGET_STAMP).
TARGET_STAMP $(TARGET_STAMP) 对应值为 /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp, 其对应指令如下:
1 2 3 4 5 6 7 8 9 10 $(TARGET_STAMP) :: +( \ $(NO_TRACE_MAKE) $(FILELIST) ; \ MD5SUM=$$(cat $(FILELIST) $(OVERRIDELIST) | mkhash md5 | awk '{print $$1}') ; \ [ -f "$@ .$$MD5SUM" ] || { \ rm -f $@ .*; \ touch $@ .$$MD5SUM; \ touch $@ ; \ } \ )
变量替换后为:
1 2 3 4 5 6 7 8 9 10 /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp:: +( \ make V=ss /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133; \ MD5SUM=$(cat /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 /home/litreily/openwrt/tmp/info/.overrides-packageinfo-2109133 | mkhash md5 | awk '{print $1}') ; \ [ -f "/home/litreily/openwrt/tmp/info/.files-packageinfo.stamp.$MD5SUM" ] || { \ rm -f /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp.*; \ touch /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp.$MD5SUM; \ touch /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp; \ } \ )
其中 make V=ss /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 又会启动一个新的子进程。
说明: 该子进程和 make V=s 一样,也会调用主Makefile,并导入 toplevel.mk 等Makefile,也就是说,如果缺少基本的编译工具或者依赖 (如 prepare-tmpinfo, .config 等),这个子进程同样会和 make V=s 一样把所需依赖都生成一遍。但是不会完整编译项目。
TARGET_STAMP 目标主要是生成依赖工具和一个MD5文件。该子进程执行结束后,会计算生成一个 package 列表文件对应的 MD5 文件,并生成目标文件 /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp.
.packageinfo-$(package) TARGET_STAMP 生成结束后,就开始调用 tmp/info/.files-packageinfo.mk 逐个生成 tmp/info/.packageinfo-$(package) 文件。这里也就是调用上述 PackageDir 的地方。
所有相关文件都存储在 tmp/info/ 目录,文件名为 .packageinfo-$(package), 每个文件保存的信息由各自目录的 Makefile 决定,前面已经给出了 base-files 目录 dump 出来的信息,主要是描述信息、DEPENDs信息等。
收集这些信息的时候,每个package都会打印一条log。
1 2 3 4 5 Collecting package info: package/base-files Collecting package info: package/boot/arm-trusted-firmware-mvebu Collecting package info: package/boot/arm-trusted-firmware-rockchip Collecting package info: package/boot/arm-trusted-firmware-sunxi #...
打印 log 使用的是 progress 函数,其定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 ifeq ($(IS_TTY) ,1) ifneq ($(strip $(NO_COLOR) ) ,1) define progress printf "\033[M\r$(1)" >&2; endef else define progress printf "\r$(1)" >&2; endef endif else define progress :; endef endif
实际上就是将 log 打印到 stderr, 也就是终端屏幕上,由于使用了 \r ,所以打印信息时会在同一行刷新,把它去掉就可以逐行打印了。
说明: 为什么 TARGET_STAMP 之后是生成 .packageinfo-$(package)? 这是因为在执行 .packageinfo 相关指令前,scan.mk 通过 include 导入了 tmp/info/.files-packageinfo.mk 文件, 该 Makefile 在导入的时候通过 $(eval $(call PackageDir,base-files,base-files,)) 系列语句定义了 .packageinfo 的大量依赖,其依赖也就是这里提到的 .packageinfo-$(package),所以,作为 .packageinfo 的依赖文件,当然要在执行目标指令前先生成。
.packageinfo 目标 all: tmp/.packageinfo 的依赖文件都准备好后,继续来看目标 all 的编译规则:
1 2 3 4 5 /home/litreily/openwrt/tmp/.packageinfo: /home/litreily/openwrt/tmp/info/.files-packageinfo.stamp $(call progress,Collecting package info: merging...) -cat /home/litreily/openwrt/tmp/info/.files-packageinfo-2109133 | awk '{gsub(/\//, "_" , $0);print "/home/litreily/openwrt/tmp/info/.packageinfo-" $0}' | xargs cat > /home/litreily/openwrt/tmp/.packageinfo 2>/dev/null $(call progress,Collecting package info: done) echo
以上指令其实很简单,就是将前面生成的 package 信息文件根据特定格式全部写入到目标文件 .packageinfo 中。同时使用 progress 函数打印相关信息。
1 2 Collecting package info: merging... Collecting package info: done
言归正传,目标 all 在生成文件 tmp/.packageinfo 后就结束了,同样 scan.mk 的任务也完成了。
小结 本文详细描述了 openwrt scan.mk 扫描过程,其目的是生成编译 package, target 所需的临时文件,将 package, target 相关的依赖信息、路径信息、描述信息存入文件,并保存在 tmp 目录。
openwrt 的 Makefile 非常复杂,许多复杂对象的依赖和指令可能相互嵌套和递归调用,所以无法完全讲述清楚,本文旨在根据 Makefile 梳理编译流程,某些细节可能无法避免被遗漏。
学习过程中用到了以下的小技巧,也在此总结一下:
某些嵌套的 make 指令隐藏了调试信息,可以修改该指令,替换或添加 -d DEBUG=vltrd
openwrt 的 make 大多调用了 NO_TRACE_MAKE, 所以可以直接在该变量定义处添加调试参数
使用 $(warning info) 打印调试信息可以帮助理解
include 指令前添加的 - 符号代表如果该文件暂时不存在可以继续执行,不必报错
有时候可以手动执行某些内嵌的 make 指令, 不过记得加上必要的全局变量,比如 TOPDIR, SCAN_COOKIE 等