(一)openwrt make menuconfig流程分析
最近探索了一下,在openwrt中,当我们输入make menuconfig命令时的,程序都做了哪些工作,相关的配置文件是如何生成的,文件之间有什么依赖关系,它的工作流程又是怎样的,它又是如何为下一步make命令做好准备工作的?带着种种疑问,开始了我的分析。我主要是按照目标之间的依赖关系,及规则的执行流程一步步进行分析的,下面是我总结的分析过程,希望与大家分享,共同学习,共同成长,其中可能会有一些理解不正确的地方,还望各位不吝指教,谢谢,^_^。
当我们在顶层目录输入make menuconfig时,由于指定的目标为menuconfig,所以make会去寻找文件中menuconfig所在的地方,然后去执行其相应规则。我们可以看到,顶层的makefile中包含了$(TOPDIR)/include/debug.mk、$(TOPDIR)/include/depends.mk、$(TOPDIR)/include/toplevel.mk,menuconfig目标就在include/toplevel.mk中,下面是它的依赖和规则。其中$(TOPDIR)就是顶层目录,定义在主Makefile中TOPDIR:=${CURDIR}。
menuconfig: scripts/config/mconf prepare-tmpinfo FORCE
if [ \! -e .config -a -e $(HOME)/.openwrt/defconfig ]; then \
cp $(HOME)/.openwrt/defconfig .config; \
fi
$< Config.in
接下来会依次分析menuconfig目标的依赖目标和规则。
scripts/config/mconf:
@$(_SINGLE)$(SUBMAKE) -s -C scripts/config all CC="$(HOSTCC_WRAPPER)"
我们将其简化一下如下,即可以看到make会去执行scripts/config/Makefile文件,并指定该文件的目标为all:
scripts/config/mconf:
@make -s -C scripts/config all CC="$(HOSTCC_WRAPPER)"
在scripts/config/Makefile文件中,目标all依赖如下:
all: conf mconf
依赖目标conf又依赖conf.c、zconf.tab.c等文件,依赖目标mconf依赖conf.c、zconf.tab.c等文件,其实这里就是要编译conf、mconf可执行文件。在后面讲到绘制图形配置菜单时,要用到mconf。小结:scripts/config/mconf这个依赖目标作用就是生成conf、mconf可执行文件,供后面程序使用。
依赖目标prepare-tmpinfo的定义如下:
prepare-tmpinfo: FORCE
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_DEPS="$(TOPDIR)/include/package*.mk $(TOPDIR)/overlay/*/*.mk" 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_DEPS="profiles/*.mk $(TOPDIR)/include/kernel*.mk $(TOPDIR)/include/target.mk" 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/metadata.pl $${type}_config "$$f" > "$$t" || { rm -f "$$t"; echo "Failed to build $$t"; false; break; }; \
done
[ tmp/.config-feeds.in -nt tmp/.packagefeeds ] || ./scripts/feeds feed_config > tmp/.config-feeds.in
./scripts/metadata.pl package_mk tmp/.packageinfo > tmp/.packagedeps || { rm -f tmp/.packagedeps; false; }
./scripts/metadata.pl package_feeds tmp/.packageinfo > tmp/.packagefeeds || { rm -f tmp/.packagefeeds; false; }
touch $(TOPDIR)/tmp/.build
1)第2行,规则首先创建一个tmp/info目录
2)第3~4行,看着比较多,我们将其简化为如下,可以看到,就是去执行scan.mk这个makefile文件,SCAN_TARGET之后是传递给scan.mk的参数,我只列出了部分。
make V=s -j1 -r -s -f include/scan.mk SCAN_TARGET="packageinfo" SCAN_DIR="package"...
make V=s -j1 -r -s -f include/scan.mk SCAN_TARGET="targetinfo" SCAN_DIR="target/linux"...
上面两条命令几乎是一样的,我们分析其中第一条,来看一下scan.mk文件,该文件最终目标为all。
2.1)在分析all目标前,我们首先看一下该文件中的如下代码,因为这部分代码会在执行all目标规则之前执行。
$(FILELIST):
rm -f $(TMP_DIR)/info/.files-$(SCAN_TARGET)-*
$(call FIND_L, $(SCAN_DIR)) $(SCAN_EXTRA) -mindepth 1 $(if $(SCAN_DEPTH),-maxdepth $(SCAN_DEPTH)) -name Makefile | xargs grep -HE 'call (Build/DefaultTargets|Build(Package|Target)|.+Package)' | sed -e 's#^$(SCAN_DIR)/##' -e 's#/Makefile:.*##' | uniq > $@
$(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 -v deps="$$DEPS" '{ \
info=$$0; \
gsub(/\//, "_", info); \
print "$ $(eval $$(call PackageDir," info "," $$0 "))"; \
} ' < $<; \
true; \
) > $@
-include $(TMP_DIR)/info/.files-$(SCAN_TARGET).mk
2.1.1)第16行,首先make会执行这行命令,所以我们先看下它,简化后如下:
-include $(TOPDIR)/tmp/info/.files-targetinfo.mk
这行意思比较清楚,就是读取.files-targetinfo.mk这个Makefile文件,如果该文件存在,那么就读取成功,否则会执行第5行的目标。$(TOPDIR)/tmp/info/.files-targetinfo-$(SCAN_COOKIE):
rm -f $(TOPDIR)/tmp/info/.files-targetinfo-*
$(call FIND_L, package) -mindepth 1 -maxdepth 5 -name Makefile | xargs grep -HE 'call (Build/DefaultTargets|Build(Package|Target)|.+Package)' | sed -e 's#^package/##' -e 's#/Makefile:.*##' | uniq > $@
其中,SCAN_COOKIE?=$(shell echo $ $ $ $)定义在include/toplevel.mk中,$ $ $ $表示进程号。FIND_L=/usr/bin/find -L $(1)定义在tmp/.host.mk中。
这里的规则就是先删除$(TOPDIR)/tmp/info目录中以.files-targetinfo-开头的文件,然后用find搜索package目录下的Makefile文件,搜索深度1~5,再经过一系列正则式子处理后,重定向到$(TOPDIR)/tmp/info/.files-targetinfo-$(SCAN_COOKIE)目标文件中,生成的文件内容如下:
network/config/ltq-adsl-app
network/config/qos-scripts
...
system/procd
kernel/brcm2708-gpu-fw
kernel/om-watchdog
...
2.1.3)第5~14行,经过正则式子处理后,会生成目标文件$(TOPDIR)/tmp/info/.files-targetinfo.mk,内容如下:
DEPS_package/kernel/linux/Makefile=modules/*.mk $(TOPDIR)/target/linux/*/modules.mk
$(eval $(call PackageDir,network_config_ltq-adsl-app,network/config/ltq-adsl-app))
$(eval $(call PackageDir,network_config_qos-scripts,network/config/qos-scripts))
$(eval $(call PackageDir,network_config_firewall,network/config/firewall))
$(eval $(call PackageDir,network_config_gre,network/config/gre))
...
此时,还生成了以下这些文件,我们在后面会讲到,它们会被merge到一个叫$(TOPDIR)/tmp/.packageinfo文件中。
.packageinfo-network_config_firewall
.packageinfo-network_config_gre
.packageinfo-network_config_ltq-adsl-app
...
2.2)all目标的分析
all目标依赖关系如下:
all: $(TMP_DIR)/.$(SCAN_TARGET)
其依赖目标展开如下:
all: $(TOPDIR)/tmp/.packageinfo
2.2.1)$(TOPDIR)/tmp/.packageinfo又依赖下面两个目标
$(TMP_DIR)/.$(SCAN_TARGET): $(TARGET_STAMP) $(SCAN_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
2.2.1.1)第一个依赖目标$(TARGET_STAMP)在scan.mk文件开始处定义的
TARGET_STAMP:=$(TMP_DIR)/info/.files-$(SCAN_TARGET).stamp
将其简化为:
TARGET_STAMP:=$(TOPDIR)/tmp/info/.files-packageinfo.stamp
该目标的依赖如下:
$(TARGET_STAMP)::
+( \
$(NO_TRACE_MAKE) $(FILELIST); \
MD5SUM=$$(cat $(FILELIST) | (md5sum || md5) 2>/dev/null | awk '{print $$1}'); \
[ -f "$@.$$MD5SUM" ] || { \
rm -f $@.*; \
touch $@.$$MD5SUM; \
touch $@; \
} \
)
上面双冒号表示,对于一个没有依赖而只有规则的目标时,即$(TARGET_STAMP)目标,当引用此目标时,规则的命令将会被无条件执行。而普通单冒号规则,当规则的目标文件存在时,此规则的命令永远不会被执行(目标文件永远是最新的)。换言之,对双冒号来说,不管$(TARGET_STAMP)文件是否存在,其规则都会被执行;而单冒号来说,$(TARGET_STAMP)文件若存在,其规则就不会被执行。 第4行,根据$(FILELIST)文件计算报文摘要值MD5SUM,例如得到的值:3996604f33f70af7d720ab4a5f9fe1f8。
第5~8行,表示若$(TOPDIR)/tmp/info/.files-packageinfo.stamp文件不存在,就创建它,以及$(TOPDIR)/tmp/info/.files-packageinfo.stamp.3996604f33f70af7d720ab4a5f9fe1f8文件。
2.2.1.2)第二个依赖目标$(SCAN_STAMP)我没有找到它的定义,它应该是一个空的值。
2.2.1.3)$(TOPDIR)/tmp/.packageinfo目标的规则中,
第2、4行,会调用process变量打印Collecting package info: merging...、Collectingpackage info: done信息。
第3行,将 2.1.3)中得到的.packageinfo-network_config_firewall等文件内容,依次取出并重定向到$(TOPDIR)/tmp/.packageinfo目标文件中,生成的目标文件内容如下:
Source-Makefile: package/network/config/ltq-adsl-app/Makefile
Package: ltq-adsl-app
Menu: 1
Version: 3.24.4.4-2
...
John Crispin
@@
Source-Makefile: package/network/config/qos-scripts/Makefile
Package: qos-scripts
Version: 1.2.1-7
...
3)scan.mk文件执行完后,接下来继续执行第5~8行的for语句中的命令,我们简化一下当type变量为package时要执行的命令:
f=tmp/.packageinfo; t=tmp/.config-package.in;
[ tmp/.config-package.in -nt tmp/.packageinfo ] || ./scripts/metadata.pl package_config tmp/.packageinfo > tmp/.config-package.in || { rm -f "$$t"; echo "Failed to build $$t"; false; break; };
先给变量f、t分别赋值,然后再判断tmp/.config-package.in文件是否比tmp/.packageinfo文件修改时间更加新,若不是则由scripts/metadata.pl脚本根据刚才2.2.1.3)中得到的tmp/.packageinfo生成tmp/.config-package.in,内容如下:
menuconfig IMAGEOPT
bool "Image configuration"
default n
source "package/*/image-config.in"
menu "Base system"
config PACKAGE_base-files
tristate "base-files................................... Base filesystem for OpenWrt"
default y if DEFAULT_base-files
default m if ALL
select PACKAGE_libc
select PACKAGE_netifd
select PACKAGE_procd
select PACKAGE_jsonfilter
select PACKAGE_librt if USE_EGLIBC
select PACKAGE_libpthread if USE_EGLIBC
help
This package contains a base filesystem and system scripts for OpenWrt.
http://openwrt.org/
config PACKAGE_block-mount
...
4)第9行,判断tmp/.config-feeds.in文件是否比tmp/.packagefeeds文件修改时间更加新,若不是则由scripts/feeds脚本
生成tmp/.config-feeds.in。
5)第10行,由scripts/metadata.pl脚本根据tmp/.packageinfo生成tmp/.packagedeps,内容如下:
package-$(CONFIG_PACKAGE_6in4) += network/ipv6/6in4
package-$(CONFIG_PACKAGE_6rd) += network/ipv6/6rd
package-$(CONFIG_PACKAGE_6to4) += network/ipv6/6to4
package-$(CONFIG_PACKAGE_8021xd) += ramips/applications/8021xd
...
这个文件最终被包含在package/Makefile中,当我们配置了对应选项后,在编译的时候,选项后对应的目录中makefile就会被执行。比如CONFIG_PACKAGE_8021xd选项被配置后,package/ramips/applications/8021xd目录中Makefile被执行,去编译该目录下的文件,生成8021xd工具。
-include $(TMP_DIR)/.packagedeps
6)第11行,由scripts/metadata.pl脚本根据tmp/.packageinfo生成tmp/.packagefeeds,这是一个空文件,用于第9行中与tmp/.config-feeds.in文件做修改时间比较的。
小结:
(1)prepare-tmpinfo这个依赖目标作用就是先生成tmp/.packageinfo、tmp/.targetinfo文件,再根据它们得到tmp/.config-package.in、tmp/.config-target.in这两个config文件,tmp/.config-feeds.in config文件,这些文件最终会由主目录中的Config.in文件读取,供mconf程序使用。
tmp/.config-package.in由主目录下onfig.in读取:source "tmp/.config-package.in"
tmp/.config-target.in由target/Config.in读取:source "tmp/.config-target.in"
tmp/.config-feeds.in由package/base-files/image-config.in读取:source "tmp/.config-feeds.in"
(2)prepare-tmpinfo这个依赖目标作用还生成了相应的makefile文件tmp/.packagedeps。
FORCE:
.PHONY: FORCE
可以看到这个依赖目标是一个伪目标,它没有依赖和规则,根据规则,该目标总会被认为是最新的,所以依赖它的目标的规则总会被执行。也就是说依赖FORCE的menuconfig目标的规则总会被执行。
小结:FORCE这个依赖目标作用就是要让依赖它的目标总会被执行。
scripts/config/mconf Config.in
所以该行表示执行mconf可执行文件,并传入Config.in参数,mconf是在第一个依赖目标scripts/config/mconf中生成的。mconf会解析Config.in文件,以及顶层目录下的.config,并绘制图形界面供我们进行相关选项的配置。然后在我们保存退出时,.config就会被及时的更新,供我们接下来make编译时使用。
menuconfig
|--scripts/config/mconf
|--prepare-tmpinfo
|--FORCE