openwrt Makefile subdir.mk 详解

之前讲述了 openwrt Makefile 的整体框架,主要包括主 Makefile 的描述,,最后简单描述了 subdir.mk , 本文就来详细剖析这个文件。

但是在解析 subdir.mk 之前,先来看两个 Makefile,

  • debug.mk: 这也是主 Makefile 引入的第一个 .mk 文件,这里定义的调试函数在 subdir.mk 中被调用
  • target/Makefile: 这是调用到 subdir.mk 的首个 Makefile, 本文将以此为例进行说明

debug.mk

debug.mk 定义了几个调试函数。

  • debug
  • warn
  • debug_eval
  • warn_eval
define debug
$$(findstring $(2),$$(if $$(DEBUG_SCOPE_DIR),$$(if $$(filter $$(DEBUG_SCOPE_DIR)%,$(1)),$(build_debug)),$(build_debug)))
endef

define warn
$$(if $(call debug,$(1),$(2)),$$(warning $(3)))
endef

define debug_eval
$$(if $(call debug,$(1),$(2)),$(3))
endef

define warn_eval
$(call warn,$(1),$(2),$(3)        $(4))
$(4)
endef

其中:

  • DEBUG_SCOPE_DIR: 默认没有定义, 可以在执行 make 时指定
  • build_debug: 对应的是 $(DEBUG) 的值,是 "dltvr" 字符串的子集

要使这几个函数生效,需要定义变量 DEBUG, 可以在执行 make 时定义.

make DEBUG=d V=s
# debug flags:
#
# d: show subdirectory tree
# t: show added targets
# l: show legacy targets
# r: show autorebuild messages
# v: verbose (no .SILENCE for common targets)

默认情况下,DEBUG 是没有定义的,通常不需要管,之所以在这拎出来,是因为 subdir.mk 有调用以上提及的调试函数。

target/Makefile

target/Makefiletools, toolchain, package 目录的 Makefile 使用的是相同的结构,通过调用 subdir.mk 内定义的两个函数 stampfile, subdir 动态生成编译目标规则。

curdir:=target

$(curdir)/subtargets:=install
$(curdir)/builddirs:=linux sdk imagebuilder toolchain
$(curdir)/builddirs-default:=linux
$(curdir)/builddirs-install:=linux $(if $(CONFIG_SDK),sdk) $(if $(CONFIG_IB),imagebuilder) $(if $(CONFIG_MAKE_TOOLCHAIN),toolchain)

$(curdir)/sdk/install:=$(curdir)/linux/install
$(curdir)/imagebuilder/install:=$(curdir)/linux/install

$(eval $(call stampfile,$(curdir),target,prereq,.config))
$(eval $(call stampfile,$(curdir),target,compile,$(TMP_DIR)/.build))
$(eval $(call stampfile,$(curdir),target,install,$(TMP_DIR)/.build))

$($(curdir)/stamp-install): $($(curdir)/stamp-compile) 

$(eval $(call subdir,$(curdir)))

其中 curdir 代表当前目录. 该 Makefile 前半部分定义了一些变量,这些变量在后续的 stampfile, subdir 函数中被调用。target, package 等目录的 Makefile 都是先调用 stampfile 生成特定目标的依赖和指令,然后再调用 subdir 生成各个子目录下目标的编译规则。至于具体怎么生成,继续往后看。

subdir.mk

接下来看本文的主角 —— subdir.mk, 这是编译项目过程中 非常重要 的文件,它可以动态定义和生成编译子目录相关目标的规则,包括 package, target, tools, toolchain 等子目录相关目标的规则。 subdir.mk 也是编译生成 world 目标必不可少的文件,主 Makefileinclude subdir.mk 之后会 include 以下 Makefile:

  • target/Makefile
  • package/Makefile
  • tools/Makefile
  • toolchain/Makefile

这些子目录的 Makefile 会调用 subdir.mk 定义的以下两个函数:

  • subdir
  • stampfile

这两个函数是配合起来一起用的,先通过 stampfile 生成 stamp-$(target) 相关规则,然后通过 subddir 生成各个子目录相关目标的规则(如 target/linux/clean)。

stampfile

先来看 stampfile.

# Parameters: <subdir> <name> <target> <depends> <config options> <stampfile location>
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

首行注释了 stampfile 的参数列表,以 target/Makefile 首个 stampfile 调用为例。

# Parameters: <subdir> <name> <target> <depends> <config options> <stampfile location>
$(eval $(call stampfile,$(curdir),target,prereq,.config))
# subdir: $(curdir) - "target"
# name: "target"
# target: "prereq"
# depends: ".config"
# config options: ""
# stampfile location: ""

为了方便调试,可以在该行前添加一行, 将 eval 改成 warning,这样就可以在编译 log 中看到解析后的编译规则啦。

# add below function call to get the details of stampfile
$(warning $(call stampfile,$(curdir),target,prereq,.config))
$(eval $(call stampfile,$(curdir),target,prereq,.config))

ok, 接下来清除 所有 编译结果,然后重新编译 defconfig, 编译过程就会调用 target/Makefile 并执行以上语句。

make distclean
make -d DEBUG=vltdr defconfig > log 2>&1

截取 log 中打印以上 warning 信息的部分如下:

target/stamp-prereq:=/home/litreily/openwrt/staging_dir/target-_/stamp/.target_prereq
$(target/stamp-prereq): /home/litreily/openwrt/tmp/.build .config
        @+/home/litreily/openwrt/scripts/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 $(findstring v,$(if $(DEBUG_SCOPE_DIR),$(if $(filter $(DEBUG_SCOPE_DIR)%,target),vltdr),vltdr)),,.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)

由于默认没有 .config 文件,所以解析出来的 STAGING_DIRtarget-_, 如果是 .config 已存在的情况下,得到的结果更完整,当然这不是我们研究的重点。

从以上解析结果可以看出,stampfile 会生成 4 个目标规则:

  • $(target/stamp-prereq) : 依赖 tmp/.build, .config
  • .SILENT: 是在没有定义 DEBUG 参数时才会生成的目标,其依赖就是刚定义的 $(target/stamp-prereq), 可以忽略
  • .PRECIOUS: 根据注释像是解决某个 bug 的 workaround, 可以忽略
  • target/stamp-prereq/clean: 用来删除目标 $(target/stamp-prereq) 的伪目标

根据分析,以上结果可以进一步简化。

target/stamp-prereq:=/home/litreily/openwrt/staging_dir/target-_/stamp/.target_prereq
$(target/stamp-prereq): /home/litreily/openwrt/tmp/.build .config
        @+/home/litreily/openwrt/scripts/timestamp.pl -n $(target/stamp-prereq) target .config || make  $(target/flags-prereq) target/prereq
        @mkdir -p $$(dirname $(target/stamp-prereq))
        @touch $(target/stamp-prereq)

target//clean:=target/stamp-prereq/clean
target/stamp-prereq/clean: FORCE
        @rm -f $(target/stamp-prereq)

同理也可以分析 target/Makefile 中剩余两个 stampfile 调用。

$(warning $(call stampfile,$(curdir),target,compile,$(TMP_DIR)/.build))
$(eval $(call stampfile,$(curdir),target,compile,$(TMP_DIR)/.build))
$(warning $(call stampfile,$(curdir),target,install,$(TMP_DIR)/.build))
$(eval $(call stampfile,$(curdir),target,install,$(TMP_DIR)/.build))

同样进行简化,得到编译目标及其规则:

# $(eval $(call stampfile,$(curdir),target,compile,$(TMP_DIR)/.build))
target/stamp-compile:=/home/litreily/openwrt/staging_dir/target-mips_24kc_musl/stamp/.target_compile
$(target/stamp-compile): /home/litreily/openwrt/tmp/.build /home/litreily/openwrt/tmp/.build
        @+/home/litreily/openwrt/scripts/timestamp.pl -n $(target/stamp-compile) target /home/litreily/openwrt/tmp/.build || make  $(target/flags-compile) target/compile
        @mkdir -p $$(dirname $(target/stamp-compile))
        @touch $(target/stamp-compile)

target//clean:=target/stamp-compile/clean
target/stamp-compile/clean: FORCE
        @rm -f $(target/stamp-compile)

# $(eval $(call stampfile,$(curdir),target,install,$(TMP_DIR)/.build))
target/stamp-install:=/home/litreily/openwrt/staging_dir/target-mips_24kc_musl/stamp/.target_install
$(target/stamp-install): /home/litreily/openwrt/tmp/.build /home/litreily/openwrt/tmp/.build
        @+/home/litreily/openwrt/scripts/timestamp.pl -n $(target/stamp-install) target /home/litreily/openwrt/tmp/.build || make  $(target/flags-install) target/install
        @mkdir -p $$(dirname $(target/stamp-install))
        @touch $(target/stamp-install)

target//clean:=target/stamp-install/clean
target/stamp-install/clean: FORCE

注意: $(target/stamp-compile)$(target/stamp-install) 的依赖包含两个重复的 tmp/.build, 看起来像是冗余的,或许可以将 target/Makefile 优化下。

分析到这一步,stampfile 对 target 目录生成的目标已经都解析出来了,就是下面这些.

$(target/stamp-prereq):
target/stamp-prereq/clean:
$(target/stamp-compile):
target/stamp-compile/clean:
$(target/stamp-install):
target/stamp-install/clean:

这些目标在主 Makefile 是作为其它目标的依赖,也是目标 world 编译过程中的中间依赖目标。

注意: 每个 stampfile 除了生成目标规则都,都定义了一个 target//clean 变量,这个在 subdir 中会调用到。

ok, stampfile 分析完了,接下来看下 subdir 要完成哪些工作。

subdir

# 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))) \
                        || $(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,$(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))))
                $(call log_make,$(1)/$(bd),$(target),,$(filter-out __default,$(variant))) \
                        || $(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

别看这么复杂,其中很大一部分是 debug 信息,只有在 DEBUG 值不为空的情况下才会打印,否则就是空值。在编译的时候加上 DEBUG=vltdr, 可以看到以下详细的调试信息,也能方便理解。

target/Makefile:23: D target
target/Makefile:23: BD target/linux
target/Makefile:23: T    target/linux/clean:  target/stamp-install/clean
target/Makefile:23: T    target/linux/download:  
target/Makefile:23: T    target/linux/prepare:  
target/Makefile:23: T    target/linux/compile:  
target/Makefile:23: T    target/linux/update:  
target/Makefile:23: T    target/linux/refresh:  
target/Makefile:23: T    target/linux/prereq:  
target/Makefile:23: T    target/linux/dist:  
target/Makefile:23: T    target/linux/distcheck:  
target/Makefile:23: T    target/linux/configure:  
target/Makefile:23: T    target/linux/check:  
target/Makefile:23: T    target/linux/check-depends:  
target/Makefile:23: T    target/linux/install:  
target/Makefile:23: BD target/sdk
target/Makefile:23: T    target/sdk/clean:  target/stamp-install/clean
target/Makefile:23: T    target/sdk/download:  
target/Makefile:23: T    target/sdk/prepare:  
target/Makefile:23: T    target/sdk/compile:  
target/Makefile:23: T    target/sdk/update:  
target/Makefile:23: T    target/sdk/refresh:  
target/Makefile:23: T    target/sdk/prereq:  
target/Makefile:23: T    target/sdk/dist:  
target/Makefile:23: T    target/sdk/distcheck:  
target/Makefile:23: T    target/sdk/configure:  
target/Makefile:23: T    target/sdk/check:  
target/Makefile:23: T    target/sdk/check-depends:  
target/Makefile:23: T    target/sdk/install: target/linux/install 
target/Makefile:23: BD target/imagebuilder
target/Makefile:23: T    target/imagebuilder/clean:  target/stamp-install/clean
target/Makefile:23: T    target/imagebuilder/download:  
target/Makefile:23: T    target/imagebuilder/prepare:  
target/Makefile:23: T    target/imagebuilder/compile:  
target/Makefile:23: T    target/imagebuilder/update:  
target/Makefile:23: T    target/imagebuilder/refresh:  
target/Makefile:23: T    target/imagebuilder/prereq:  
target/Makefile:23: T    target/imagebuilder/dist:  
target/Makefile:23: T    target/imagebuilder/distcheck:  
target/Makefile:23: T    target/imagebuilder/configure:  
target/Makefile:23: T    target/imagebuilder/check:  
target/Makefile:23: T    target/imagebuilder/check-depends:  
target/Makefile:23: T    target/imagebuilder/install: target/linux/install 
target/Makefile:23: BD target/toolchain
target/Makefile:23: T    target/toolchain/clean:  target/stamp-install/clean
target/Makefile:23: T    target/toolchain/download:  
target/Makefile:23: T    target/toolchain/prepare:  
target/Makefile:23: T    target/toolchain/compile:  
target/Makefile:23: T    target/toolchain/update:  
target/Makefile:23: T    target/toolchain/refresh:  
target/Makefile:23: T    target/toolchain/prereq:  
target/Makefile:23: T    target/toolchain/dist:  
target/Makefile:23: T    target/toolchain/distcheck:  
target/Makefile:23: T    target/toolchain/configure:  
target/Makefile:23: T    target/toolchain/check:  
target/Makefile:23: T    target/toolchain/check-depends:  
target/Makefile:23: T    target/toolchain/install:  
target/Makefile:23: T    target/clean:  target/linux/clean
target/Makefile:23: T    target/download:  target/linux/download
target/Makefile:23: T    target/prepare:  target/linux/prepare
target/Makefile:23: T    target/compile:  target/linux/compile
target/Makefile:23: T    target/update:  target/linux/update
target/Makefile:23: T    target/refresh:  target/linux/refresh
target/Makefile:23: T    target/prereq:  target/linux/prereq
target/Makefile:23: T    target/dist:  target/linux/dist
target/Makefile:23: T    target/distcheck:  target/linux/distcheck
target/Makefile:23: T    target/configure:  target/linux/configure
target/Makefile:23: T    target/check:  target/linux/check
target/Makefile:23: T    target/check-depends:  target/linux/check-depends
target/Makefile:23: T    target/install:  target/linux/install

其中:

  • D : Directory, 当前目录
  • BD : builddirs, 在 target/Makefile 中定义的 builddirs 变量
  • T : subtargets, 具体的目标

结合以上调试信息可以更容易分析 subdir 的执行流程,函数内部主要通过 3 层 foreach 循环逐层遍历以下信息:

$(call warn,$(1),d,D $(1))
# 1. builddirs: linux sdk imagebuilder toolchain`
$(foreach bd,$($(1)/builddirs),
  $(call warn,$(1),d,BD $(1)/$(bd))
  # 2. subtargets: clean download prepare compile update refresh prereq dist distcheck configure check check-depends install
  $(foreach target,$(SUBTARGETS) $($(1)/subtargets),
     # 3. buildtypes: ""
     $(foreach btype,$(buildtypes-$(bd)),

  $(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(target): $(if $(NO_DEPS)$(QUILT),,$($(1)/$(bd)/$(target)) $(call $(1)//$(target),$(1)/$(bd))))
        # 3. variant: __default
        $(foreach variant, ...
#...
$(foreach target,$(SUBTARGETS) $($(1)/subtargets),$(call subtarget,$(1),$(target)))

对其中的个别变量进行说明,$(bd) 对应 target 子目录 linux, sdk 等;$(SUBTARGETS) 是在 rules.mk 中定义的。

# include/subdir.mk
SUBTARGETS:=$(DEFAULT_SUBDIR_TARGETS)

# rules.mk
DEFAULT_SUBDIR_TARGETS:=clean download prepare compile update refresh prereq dist distcheck configure check check-depends

遍历 target 的语句是 $(foreach target, $(SUBTARGETS) $(1)/subtargets), 就是说,除了上面的默认 targets 之外,还要加上 $(1)/subtargets 的值,对应 target/Makefile 中的值是 install. 这与上面展示的 log 是相对应的。

整体结构清楚了,但是还是不好分析,那么先根据以下原则对 subdir 进行精简:

  1. 忽略调试信息 warn
  2. 解析变量
  3. 解析 if 条件判断,仅留下有效分支

举例说明,

  1. subdir 中的 buildtypes-$(bd) 为空,所以其对应 foreach 可以直接删除;
  2. BUILD_VARIANT 未定义,对应 if 判断结果为 __default;
  3. $(BUILD_LOG) 未定义,对应 if 分支删除。
  4. $($(1)/autoremove) 未定义,对应 if 分支删除。

根据这些规则后简化的 subdir 如下:

# Parameters: <subdir>
define subdir
  $(foreach bd,$($(1)/builddirs),
    $(foreach target,$(SUBTARGETS) $($(1)/subtargets),
      $(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(target): $(call $(1)//$(target),$(1)/$(bd)))
        $(foreach variant,__default,
                $(call log_make,$(1)/$(bd),$(target),,$(filter-out __default,$(variant))) 
        )
        # 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

example

以遍历 $(bd) 值为 linux 说明以上精简后的操作。

      $(call warn_eval,$(1)/$(bd),t,T,$(1)/$(bd)/$(target): $(call $(1)//$(target),$(1)/$(bd)))

在定义了 DEBUG=vltdr 的情况下,以上指令会打印 target 相关信息。

target/Makefile:23: T    target/linux/clean:  target/stamp-install/clean
target/Makefile:23: T    target/linux/download:  
target/Makefile:23: T    target/linux/prepare:  
target/Makefile:23: T    target/linux/compile:  
target/Makefile:23: T    target/linux/update:  
target/Makefile:23: T    target/linux/refresh:  
target/Makefile:23: T    target/linux/prereq:  
target/Makefile:23: T    target/linux/dist:  
target/Makefile:23: T    target/linux/distcheck:  
target/Makefile:23: T    target/linux/configure:  
target/Makefile:23: T    target/linux/check:  
target/Makefile:23: T    target/linux/check-depends:  
target/Makefile:23: T    target/linux/install:  

从前面 warn_eval 的定义可知,它除了打印 warning 信息外,还会将最后一个参数单独执行一遍。所以它包含两个功能,warneval, 且 eval 更重要。它实际上也定义了一组编译目标。也就是 warn_eval 的第4个参数:

$(1)/$(bd)/$(target): $(call $(1)//$(target),$(1)/$(bd))

将变量依次替换得到:

target/linux/clean: $(call target//clean,target/linux)
  • $(target/linux) : 变量没有定义
  • $(target//clean) : 前面 stampfile 定义的变量,其值为 target/stamp-install/clean

最终解析出来如下:

target/linux/clean: target/stamp-install/clean

该目标除了依赖之外,还有对应的指令,也就是紧随其后的 foreach 语句。

        $(foreach variant,__default,
                $(call log_make,$(1)/$(bd),$(target),,$(filter-out __default,$(variant))) 
        )

看到这应该明了了,就是这个 log_make 调用,顺藤摸瓜,来看下这个函数的定义。

subdir_make_opts = \
        -r -C $(1) \
                BUILD_SUBDIR="$(1)" \
                BUILD_VARIANT="$(4)"

# 1: subdir
# 2: target
# 3: build type
# 4: build variant
log_make = \
        $(if $(call debug,$(1),v),,@)+ \
        $(if $(BUILD_LOG), \
                set -o pipefail; \
                mkdir -p $(BUILD_LOG_DIR)/$(1)$(if $(4),/$(4));) \
      $(SCRIPT_DIR)/time.pl "time: $(1)$(if $(4),/$(4))/$(if $(3),$(3)-)$(2)" \
      $$(SUBMAKE) $(subdir_make_opts) $(if $(3),$(3)-)$(2) \
                $(if $(BUILD_LOG),SILENT= 2>&1 | tee $(BUILD_LOG_DIR)/$(1)$(if $(4),/$(4))/$(if $(3),$(3)-)$(2).txt)

同样,先进行简化,DEBUG, BUILD_LOG 均未定义, 对应分支可删除;已知全局变量可以替换掉。

log_make = \
        @+ \
        scripts/time.pl "time: $(1)$(if $(4),/$(4))/$(if $(3),$(3)-)$(2)" \
        $$(SUBMAKE) -r -C $(1) BUILD_SUBDIR="$(1)" BUILD_VARIANT="$(4)" \
        $(if $(3),$(3)-)$(2)

接着回到 subdir 中的 log_make 的调用处:

$(call log_make,$(1)/$(bd),$(target),,$(filter-out __default,$(variant))) 

按照简化后的 log_make 代入参数解析以上指令。

+ scripts/time.pl "time: target/linux/clean" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" clean

将目标、依赖及其指令组合起来就是这样的:

target/linux/clean: target/stamp-install/clean
        + scripts/time.pl "time: target/linux/clean" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" clean

看看这个目标定义,依赖项 target/stamp-install/clean 的规则在 stampfile 中定义好了;其指令是通过 perl 脚本 time.pl 执行 make 指令并记录时间信息。

到此就完成了对 subdir 遍历 subtargets 之一 target/linux/clean 的解析,其它 target 也是类似的。

# target/linux
target/linux/clean: target/stamp-install/clean
        + scripts/time.pl "time: target/linux/clean" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" clean
target/linux/download: 
        + scripts/time.pl "time: target/linux/download" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" download
target/linux/prepare: 
        + scripts/time.pl "time: target/linux/download" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" prepare
#...
target/linux/install: 
        + scripts/time.pl "time: target/linux/install" make -w -r -C target/linux BUILD_SUBDIR="target/linux" BUILD_VARIANT="" install

# target/sdk
target/sdk/clean: target/stamp-install/clean
        + scripts/time.pl "time: target/sdk/clean" make -w -r -C target/sdk BUILD_SUBDIR="target/sdk" BUILD_VARIANT="" clean
target/sdk/download: 
        + scripts/time.pl "time: target/sdk/download" make -w -r -C target/sdk BUILD_SUBDIR="target/sdk" BUILD_VARIANT="" download
#...
target/sdk/install: 
        + scripts/time.pl "time: target/sdk/install" make -w -r -C target/sdk BUILD_SUBDIR="target/sdk" BUILD_VARIANT="" install

# target/imagebuilder
target/imagebuilder/clean: target/stamp-install/clean
        + scripts/time.pl "time: target/imagebuilder/clean" make -w -r -C target/imagebuilder BUILD_SUBDIR="target/imagebuilder" BUILD_VARIANT="" clean
target/imagebuilder/download: 
        + scripts/time.pl "time: target/imagebuilder/download" make -w -r -C target/imagebuilder BUILD_SUBDIR="target/imagebuilder" BUILD_VARIANT="" download
#...
target/imagebuilder/install: 
        + scripts/time.pl "time: target/imagebuilder/install" make -w -r -C target/imagebuilder BUILD_SUBDIR="target/imagebuilder" BUILD_VARIANT="" install

# target/toolchain
target/toolchain/clean: target/stamp-install/clean
        + scripts/time.pl "time: target/toolchain/clean" make -w -r -C target/toolchain BUILD_SUBDIR="target/toolchain" BUILD_VARIANT="" clean
target/toolchain/download: 
        + scripts/time.pl "time: target/toolchain/download" make -w -r -C target/toolchain BUILD_SUBDIR="target/toolchain" BUILD_VARIANT="" download
#...
target/toolchain/install: 
        + scripts/time.pl "time: target/toolchain/install" make -w -r -C target/toolchain BUILD_SUBDIR="target/toolchain" BUILD_VARIANT="" install

in the end

最后,还有一个小尾巴没讲到。注意看前面给出 log 的最后一部分。

target/Makefile:23: T    target/clean:  target/linux/clean
target/Makefile:23: T    target/download:  target/linux/download
target/Makefile:23: T    target/prepare:  target/linux/prepare
target/Makefile:23: T    target/compile:  target/linux/compile
target/Makefile:23: T    target/update:  target/linux/update
target/Makefile:23: T    target/refresh:  target/linux/refresh
target/Makefile:23: T    target/prereq:  target/linux/prereq
target/Makefile:23: T    target/dist:  target/linux/dist
target/Makefile:23: T    target/distcheck:  target/linux/distcheck
target/Makefile:23: T    target/configure:  target/linux/configure
target/Makefile:23: T    target/check:  target/linux/check
target/Makefile:23: T    target/check-depends:  target/linux/check-depends
target/Makefile:23: T    target/install:  target/linux/install

这里打印的信息是哪里来的呢?其实也是 subdir 函数,是该函数最后一个 foreach 循环遍历出来的。

  $(foreach target,$(SUBTARGETS) $($(1)/subtargets),$(call subtarget,$(1),$(target)))

经过前面的推导,这部分简直是小 case, 无非是遍历所有 target, 然后使用函数 subtarget 去生成规则。

subtarget-default = $(filter-out ., \
        $(if $($(1)/builddirs-$(2)),$($(1)/builddirs-$(2)), \
        $(if $($(1)/builddirs-default),$($(1)/builddirs-default), \
        $($(1)/builddirs))))

define subtarget
  $(call warn_eval,$(1),t,T,$(1)/$(2): $($(1)/) $(foreach bd,$(call subtarget-default,$(1),$(2)),$(1)/$(bd)/$(2)))
endef

同样对其进行简化。

# $(1): target
# $(2): $(target) -> "clean, download, ..., install"
# $(target/builddirs-clean): ""
# $(target/builddirs-default): linux # defined in target/Makefile
subtarget-default = $(filter-out ., $(if $($(1)/builddirs-$(2)),$($(1)/builddirs-$(2)), linux))

define subtarget
  $(1)/$(2): $($(1)/) $(foreach bd,$(call subtarget-default,$(1),$(2)),$(1)/$(bd)/$(2))
endef

然后代入参数 $(1): target, $(2): clean, 得到:

subtarget-default = linux

define subtarget
  target/clean: target/linux/clean
endef

这与 log 中的一致,其它 target 的规则也是类似,这里最主要的就是一个默认编译规则,相当于在不指定具体 target 哪个子目录时,根据默认值进行编译,target/Makefile 定义的 $(builddirs-default) 就是 linux, 也就是说,执行 make target/cleanmake target/linux/clean 是一样的。

target 目录外,package, tools, toolchain 目录也是类似的。

到这,最后的小尾巴也讲完啦。

Tips

使用 makefile 的 warning 函数打印信息可以快速梳理 Makefile 的执行流程。

$(warning info)

# Example
# target/Makefile
$(warning $(call subdir,$(curdir)))

配合 make -n V=s 能够打印指令信息, 如果加上 DEBUG 可以显示更加详细的调试信息。

make -d V=s DEBUG=dltvr
# -n to print command only
make -d -n V=s DEBUG=dltvr

Reference