u-boot学习笔记系列(2) -- scripts

https://github.com/wdfk-prog/U-boot-Study

scripts 用于构建脚本的工具

scripts_basic 用于构建基本脚本的工具

参考

uboot 和 Linux kernel 的主 Makefile 里面,都有下面这段话:

PHONY += scripts_basic
scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic
	$(Q)rm -f .tmp_quiet_recordmcount

展开后就是:

make -f ./scripts/Makefile.build obj=scripts/basic

$(build)的展开

主 Makefile 里面,有一个非常重要的 include:

include scripts/Kbuild.include

这是一个头文件,相当于把 Kbuild.include 的内容直接写到主 Makefile 里。
Kbuild.include 代码片段:

###
# Shorthand for $(Q)$(MAKE) -f scripts/Makefile.build obj=
# Usage:
# $(Q)$(MAKE) $(build)=dir
build := -f $(srctree)/scripts/Makefile.build obj

这句话的实质,是变量的字符串展开;就是说 build 被当做了 Kbuild 的一个保留的字符串变量。
所以 $(Q)$(MAKE) $(build)=scripts/basic 的展开,就是把$(build) 用 := 后的字符串替换

Kbuild 为不同的编译目标生成编译规则。

Kbuild 中,几乎或者全部的 .o 都是由 Makefile.build 来完成编译的。
Makefile.build 会为不同类型的编译对象,指定不同的规则。类似于大家喜闻乐见的:

gcc -c -o hello.o hello.c

但是 Kbuild 追求的是批量化编译,所以指定规则的过程很复杂,需要认真分析下。
这篇文章,只分析 hostprogs 相关的规则生成和编译。
hostprogs 是在编译过程中主机所用到的一些工具;hostprogs 编译的输出件是运行在主机上的,最后不会被链接到目标文件中。

过滤掉 Makefile.build 中和 hostprogs 无关的代码,得:

src := $(obj)
# $(obj) 是传入的参数,即 scripts/basic
PHONY := __build
__build:
...
include scripts/Kbuild.include

kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

include scripts/Makefile.lib

hostprogs := $(sort $(hostprogs))
ifneq ($(hostprogs),)
include scripts/Makefile.host
endif
...
__build: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \
	 $(if $(KBUILD_MODULES), $(targets-for-modules)) \
	 $(subdir-ym) $(always-y)
	@:

PHONY += FORCE
FORCE:
...
.PHONY: $(PHONY)

找到子目录中的 Makefile

关于 $(kbuild-file) 的分析

# 文件名 Kbuild 优先于 Makefile
# 如果 src 是绝对路径,则使用 src 作为 kbuild-dir;否则,使用 $(srctree)/$(src) 作为 kbuild-dir
# 如果 $(kbuild-dir)/Kbuild 存在,则使用 $(kbuild-dir)/Kbuild 作为 kbuild-file;否则,使用 $(kbuild-dir)/Makefile 作为 kbuild-file
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
# 导入 kbuild-file的内容
# 例如 obj=scripts/basic srctree=. 则导入scripts/basic/Makefile文件
include $(kbuild-file)

这些语句,相当于 include 了scripts/basic/Makefile。
这个文件就一句话:

# scripts/basic/Makefile
hostprogs-always-y	+= fixdep

使用 Makefile.lib 对编译目标进行分类和整理

scripts/Makefile.lib 会对各种编译目标进行转换、抽象和提取。
编译目标的种类有很多,这里只追踪 hostprogs 相关的。

# scripts/Makefile.lib
hostprogs += $(hostprogs-always-y) $(hostprogs-always-m)
always-y += $(hostprogs-always-y) $(hostprogs-always-m)
always-y	:= $(addprefix $(obj)/,$(always-y))

这样,可以获得

hostprogs = fixdep
always-y = scripts/basic/fixdep

是的,虽然 Makefile.build 这个文件很长,但是真正和 scripts/basic 相关的,就上面 3 行代码。

Makefile.host 所做的工作

因为 hostprogs 不为空,所以引入了另一个包含

# scripts/Makefile.build
ifneq ($(hostprogs),)
include scripts/Makefile.host
endif

scripts/Makefile.host 这个文件会将 $(hostprogs) 转换为 $(host-csingle) 、$(host-cmulti) 等目标。这里需要介绍下 host-csingle、host-cmultihost-cobjshost-cxxmultihost-cxxobjs 这几个变量。这是 Makefile.host 所能处理的几类编译目标。Makefile.host 会为这几类目标生成编译规则。说到底,Kbuild 的这一套机制,目的就是为不同的编译目标生成编译规则。

host-cobjs 实例

这里只是举例,在编译 scripts/basic 时是不会用到 host-cobjs 类型的目标的。介绍 host-cobjs,只是为后面分析 host-csingle 做铺垫。以 qconf 为例,scripts/kconfig/Makefile 中的代码段:

# scripts/kconfig/Makefile
# object files used by all kconfig flavours
common-objs	:= confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o \
		   symbol.o util.o
hostprogs	+= qconf
qconf-cxxobjs	:= qconf.o qconf-moc.o
qconf-objs	:= images.o $(common-objs)

然后可得:hostprogs = qconf,也许还有其他的,此处忽略;qconf-objs = images.o confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o symbol.o util.o

Makefile.host 对 host-cobjs 的处理

# scripts/Makefile.host
# Object (.o) files compiled from .c files
host-cobjs	:= $(sort $(foreach m,$(hostprogs),$($(m)-objs)))	# 语句1
host-cobjs	:= $(addprefix $(obj)/,$(host-cobjs))	# 语句2

foreach 的作用,是将 $(hostprogs) 中的变量逐个取出,然后带入到 $($(m)-objs)) 中即得到 ( q c o n f − o b j s ) ‘ ; 注意, ‘ (qconf-objs)`;注意,` (qconfobjs);注意,(qconf-objs) 还会被展开一次,即得到images.o confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o symbol.o util.o`
所以,语句1 的结果是:

host-cobjs := images.o confdata.o expr.o lexer.lex.o parser.tab.o preprocess.o symbol.o util.o

语句2 是为上面的这些 .o 添加目录前缀。所以 $(host-cobjs) 实际上是若干 .c 对应的 .o 的集合。当为 $(host-cobjs) 制定好规则以后,上面的 images.o confdata.o ... 所对应的 images.c confdata.c ... 均会批量化的按照制定的规则进行编译。

# 这里用到了“静态模式”的概念,暂不展开
$(host-cobjs): $(obj)/%.o: $(src)/%.c FORCE
	$(call if_changed_dep,host-cobjs)

if_changed_dep,的这个逗号后面,一定不要加空格什么的;或者说 call 的用法就这样,逗号后面不要加不相关的字符;另外,对应的 cmd_cpp_lds 等命令,一定用延时赋值“=”,而不是直接赋值“:=”,因为:如果命令中用到 $@ 或者 $<时,延时赋值可以展开,直接赋值不会展开。一定要注意!!!!!

host-csingle 实例
# scripts/Makefile.host
# C code
# Executables compiled from a single .c file
host-csingle	:= $(foreach m,$(hostprogs), \
			$(if $($(m)-objs)$($(m)-cxxobjs),,$(m)))	# 语句1
host-csingle	:= $(addprefix $(obj)/,$(host-csingle))	# 语句2

此处 hostprogs = fixdep,第一次带入得 $(fixdep-objs)$(fixdep-cxxobjs),因为它们两个展开后,均为空,所以语句1 的返回结果就是 $host-csingle) = fixdep;语句2 为 fixdep 添加了前缀,$(host-csingle) = scripts/basic/fixdep;所以,$(host-csingle) 指的是那些由单个 .c 编译而成的可执行文件,比如说 fixdep;Makefile.hosthost-csingle制定了编译规则。

# Create executable from a single .c file
# host-csingle -> Executable

# OSTCC 是一个变量,通常表示用于编译主机程序的编译器(例如 gcc)。
# $@ 是一个自动化变量,表示当前目标的名称。
# $(hostc_flags):表示编译主机程序时使用的编译标志。
# $(KBUILD_HOSTLDFLAGS):表示链接主机程序时使用的链接标志。
# -o $@:指定输出文件的名称,$@ 表示当前目标的名称。
# $<:表示第一个依赖文件的名称,通常是源文件。
# $(KBUILD_HOSTLDLIBS):表示链接主机程序时使用的库文件。
# $(HOSTLDLIBS_$(@F)):表示特定目标文件的链接库,$(@F) 表示当前目标文件的文件名部分。
quiet_cmd_host-csingle 	= HOSTCC  $@
      cmd_host-csingle	= $(HOSTCC) $(hostc_flags) $(KBUILD_HOSTLDFLAGS) -o $@ $< \
	  	$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(@F))
#用于编译目标文件夹下的所有.c文件
# 会执行例如cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer   -std=gnu11       -o scripts/basic/fixdep scripts/basic/fixdep.c的命令
$(host-csingle): $(obj)/%: $(src)/%.c FORCE
#调用if_changed_dep中会调用cmd_host-csingle
	$(call if_changed_dep,host-csingle)

再回到 scripts/Makefile.build

Makefile.host 为目标制定了规则。但是,制定规则不代表调用规则。
那 scripts_basic 的规则是怎么被调用的呢?
Makefile.build 的默认编译目标是 __build:

# scripts/Makefile.build
PHONY := __build
__build:
...
__build: $(if $(KBUILD_BUILTIN), $(targets-for-builtin)) \
	 $(if $(KBUILD_MODULES), $(targets-for-modules)) \
	 $(subdir-ym) $(always-y)
	@:

其实,这里有个难以理解和追踪的地方了:
__build 中的有效依赖是 $(always-y) ,但是搜索编整个 Makefile 工程都找不到对 $(always-y) 目标的编译——没有为 $(always-y) 设计编译规则。
那么,目标是怎么被编译的呢?
前面 scripts/Makefile.host 中得到了$(host-csingle),虽然 __build 中并没有包含 $(host-csingle) ,但是 $(always-y)$(host-csingle) 包含了相同的目标 scripts/basic/fixdep。
所以 __build 对 $(always-y) 的依赖就是对 scripts/basic/fixdep 的依赖。
而 scripts/basic/fixdep 的编译规则已经被 $(host-csingle) 制定了。

dtc 用于生成设备树二进制文件

具体分析

  • 输入命令行查看构建流程
$ make scripts V=1 CROSS_COMPILE=arm-none-eabi- ARCH=arm stm32h750-art-pi_defconfig
# scripts + stm32h750-art-pi_defconfig 误识别成混合目标了
set -e; \
for i in scripts stm32h750-art-pi_defconfig; do \
        make -f ./Makefile $i; \
done
# 等同于 make scripts;但是会先执行scripts_basic 和 include/config/auto.conf (include/config/%.conf:)
# 执行include/config/%.conf:
make -f ./Makefile syncconfig

# 执行scripts_basic,由于命令是$(Q)$(MAKE) $(build)=scripts/basic
# 所以执行的是make -f ./scripts/Makefile.build obj=scripts/basic
make -f ./scripts/Makefile.build obj=scripts/basic

# 在scripts/Makefile.build中由于输入make 没有带参数,所以执行了__build;
# __build: $(always) 依赖于 $(obj)/fixdep; $(always)从include $(kbuild-file)导入,即scripts/basic/Makefile
# scripts/basic/Makefile中定义了hostprogs-y	:= fixdep;  always		:= $(hostprogs-y)
# 而$(host-csingle) 指定了编译规则
# 执行构建fixdep程序
  cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer   -std=gnu11       -o scripts/basic/fixdep scripts/basic/fixdep.c
rm -f .tmp_quiet_recordmcount

# 执行include/config/%.conf:
make -f ./scripts/Makefile.build obj=scripts/kconfig syncconfig
# 执行include/config/auto.conf
scripts/kconfig/conf  --syncconfig Kconfig
make -f ./scripts/Makefile.autoconf || \
        { rm -f include/config/auto.conf; false; }

# 执行./scripts/Makefile.autoconf 由于依赖关系优先执行create_symlink
# 这里在arch/arm/include/asm/arch中创建了一个软链接arch-stm32h7
if [ -d arch/arm/mach-stm32h7/include/mach ]; then      \
        dest=../../mach-stm32h7/include/mach;                   \
else                                                            \
        dest=arch-stm32h7;                      \
fi;                                                             \
ln -fsn $dest arch/arm/include/asm/arch

# 执行./scripts/Makefile.autoconf 由于依赖关系优先执行filechk_config_h,创建include/config.h
# 首先执行了filechk,在执行了filechk_config_h
set -e; mkdir -p include/;      
# 执行了filechk_config_h 生成了include/config.h.tmp
(echo "/* Automatically generated - do not edit */"; echo \#define CFG_BOARDDIR board/st/stm32h750-art-pi; echo \#include \<configs/"stm32h750-art-pi".h\> ; echo \#include \<asm/config.h\>; echo \#include \<linux/kconfig.h\>; echo \#include \<config_fallbacks.h\>;) < scripts/Makefile.autoconf > include/config.h.tmp;
# 如果原文件存在且与临时文件内容相同,则删除临时文件;否则,将临时文件重命名为目标文件
 if [ -r include/config.h ] && cmp -s include/config.h include/config.h.tmp; then rm -f include/config.h.tmp; else : '  UPD     include/config.h'; mv -f include/config.h.tmp include/config.h; fi
# 执行./scripts/Makefile.autoconf 由于依赖关系优先执行u-boot.cfg: 即cmd_u_boot_cfg
# 编译u-boot.cfg 并将define CONFIG_中包含CONFIG_IS_ENABLED,CONFIG_IF_ENABLED_INT,CONFIG_VAL的宏定义剔除
  arm-none-eabi-gcc $(c_flags) -DDO_DEPS_ONLY -dM include/config.h > u-boot.cfg.tmp && { grep 'define CONFIG_' u-boot.cfg.tmp | sed '/define CONFIG_IS_ENABLED(/d;/define CONFIG_IF_ENABLED_INT(/d;/define CONFIG_VAL(/d;' > u-boot.cfg; rm u-boot.cfg.tmp; } || { rm u-boot.cfg.tmp; false; }
# 执行./scripts/Makefile.autoconf 由于依赖关系优先执行include/autoconf.mk 即cmd_autoconf
# 从u-boot.cfg中提取宏定义,匹配define2mk.sed中的规则,并将提取的宏定义写入include/autoconf.mk(用于 U-Boot 常规配置)
  sed -n -f ./tools/scripts/define2mk.sed u-boot.cfg | while read line; do if [ -n "" ] || ! grep -q "${line%=*}=" include/config/auto.conf; then echo "$line"; fi; done > include/autoconf.mk
# 执行include/config/%.conf: 由于依赖关系优先执行include/autoconf.mk.dep 即cmd_autoconf_dep
  arm-none-eabi-gcc -x c -DDO_DEPS_ONLY -M -MP $(c_flags) -MQ include/config/auto.conf include/config.h > include/autoconf.mk.dep || { rm include/autoconf.mk.dep; false; }

# 执行./scripts/Makefile.autoconf
touch include/config/auto.conf


make -f ./scripts/Makefile.build obj=scripts/basic
rm -f .tmp_quiet_recordmcount
# 执行scripts_dtc
if test "./scripts/dtc/dtc" = "./scripts/dtc/dtc"; then \
        make -f ./scripts/Makefile.build obj=scripts/dtc; \
else \
        if ! ./scripts/dtc/dtc -v >/dev/null; then \
                echo '*** Failed to check dtc version: ./scripts/dtc/dtc'; \
                false; \
        else \
                if test "010406" -lt 010406; then \
                        echo '*** Your dtc is too old, please upgrade to dtc 010406 or newer'; \
                        false; \
                else \
                        if [ -n "" ]; then \
                                if ! echo "import libfdt" | python3 2>/dev/null; then \
                                        echo '*** pylibfdt does not seem to be available with python3'; \
                                        false; \
                                fi; \
                        fi; \
                fi; \
        fi; \
fi

#执行 scripts
make -f ./scripts/Makefile.build obj=scripts
# 执行scripts_basic
make -f ./scripts/Makefile.build obj=scripts/basic
rm -f .tmp_quiet_recordmcount

scripts/Kbuild 用于构建脚本的 Kbuild 文件

echo-cmd 用于控制命令的回显

  • 调用 escsq 函数,对 $(quiet)cmd_$(1) 的内容进行转义处理
  • $(echo-why) 是一个变量,通常用于附加额外的回显信息
echo-cmd = $(if $($(quiet)cmd_$(1)),\
    echo '  $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)

filechk 用于检查文件是否改变

  • 在dts/upstream/scripts/Kbuild.include中定义
  • 规则的输出应写入标准输出,新文件的内容将与现有文件进行比较。如果文件不存在,则创建新文件;如果内容不同,则使用新文件;如果内容相同,则不进行更改,也不更新时间戳。
# 设置命令执行时遇到错误即退出
# $(kecho) ' CHK $@';:打印检查文件的消息,$@ 表示目标文件
# mkdir -p $(dir $@);:创建目标文件所在的目录
# $(filechk_$(1)) < $< > $@.tmp;:执行文件检查命令,并将结果输出到临时文件
# 如果目标文件存在且与临时文件内容相同,则删除临时文件
# 否则,将临时文件重命名为目标文件
define filechk
	$(Q)set -e;				\
	$(kecho) '  CHK     $@';		\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

# filechk_$(1) 是一个宏,用于执行文件检查命令;
# 例如,filechk_config_h 宏用于检查配置文件。

# 如果 $(VENDOR) 变量存在,则使用 $(VENDOR)/,否则为空
# 如果 $(CONFIG_SYS_CONFIG_NAME) 变量存在,则包含配置文件,否则为空
define filechk_config_h
	(echo "/* Automatically generated - do not edit */";		\
	echo \#define CFG_BOARDDIR board/$(if $(VENDOR),$(VENDOR)/)$(BOARD);\
	$(if $(CONFIG_SYS_CONFIG_NAME),echo \#include \<configs/$(CONFIG_SYS_CONFIG_NAME).h\> ;) \
	echo \#include \<asm/config.h\>;				\
	echo \#include \<linux/kconfig.h\>;				\
	echo \#include \<config_fallbacks.h\>;)
endef

fixdep 用于处理依赖关系的工具

  • fixdep 是一个用于处理依赖关系的工具,通常在构建系统中使用。它解析依赖文件(depfile),生成目标文件(target)的依赖关系,并输出相应的命令行(cmdline

调用流程

  1. if_changed_dep:如果目标文件的依赖关系发生变化,则执行指定的命令行。
# Execute the command and also postprocess generated .d dependencies file.

# $(strip $(any-prereq) $(arg-check) ) 去除 $(any-prereq) 和 $(arg-check) 中的空白字符,并检查它们是否非空
# 如果 $(any-prereq) 或 $(arg-check) 非空,则执行后续的命令。
# (echo-cmd) 是一个变量,通常用于控制命令的回显。$(cmd_$(1)) 是一个变量,表示具体的命令。$(1) 是一个占位符,表示传递给 if_changed_dep 宏的参数
# depfile  =  scripts/basic/.fixdep.d
#target  =  scripts/basic/fixdep
# cmdline  =  cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer  -std=gnu11     -o scripts/basic/fixdep scripts/basic/fixdep.c  
if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ),                  \
	@set -e;                                                             \
  	echo "cmd_$(1): $(cmd_$(1))";                                        \
	echo 'call fixdep: scripts/basic/fixdep $(depfile) $@ "$(make-cmd)"';\
	scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
	rm -f $(depfile);                                                    \
	mv -f $(dot-target).tmp $(dot-target).cmd, @:)
  • 调用打印如下
$ make scripts V=1 CROSS_COMPILE=arm-none-eabi- ARCH=arm stm32h750-art-pi_defconfig
#$(host-csingle) 指定了编译规则 在scripts/Makefile.host中定义了编译规则
#而$(always-y) 从include $(kbuild-file)导入,即scripts/basic/Makefile
cmd_host-csingle: cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer   -std=gnu11       -o scripts/basic/fixdep scripts/basic/fixdep.c   
  cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer   -std=gnu11       -o scripts/basic/fixdep scripts/basic/fixdep.c   
call fixdep: scripts/basic/fixdep scripts/basic/.fixdep.d scripts/basic/fixdep "cc -Wp,-MD,scripts/basic/.fixdep.d -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer   -std=gnu11       -o scripts/basic/fixdep scripts/basic/fixdep.c   "

构建


hostprogs-y	:= fixdep
always		:= $(hostprogs-y)

# 需要 fixdep 来编译其他主机程序
# 表示除了 fixdep 之外的所有目标都依赖于 fixdep
# 过滤掉 always 变量中的 fixdep,只保留其他目标。
# 为每个目标添加前缀 $(obj)/,表示目标文件的路径
# : $(obj)/fixdep 表示 fixdep 目标文件的路径。这意味着,除了 fixdep 之外的所有目标都依赖于 fixdep,即在构建这些目标之前,必须先构建 fixdep
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep

使用方法

Usage: fixdep <depfile> <target> <cmdline>
  • <depfile>:依赖文件,包含目标文件的依赖关系。
  • <target>:目标文件,通常是需要编译的源文件。
  • <cmdline>:命令行,用于编译目标文件的命令。

源代码

/*
 * 在本程序的预期用途中,stdout 被重定向到 .*.cmd
 *文件。必须检查 printf() 和 putchar() 的返回值才能捕获
 * 任何错误,例如“No space left on device”。
 */
static void xprintf(const char *format, ...)
{
	va_list ap;
	int ret;

	va_start(ap, format);
	ret = vprintf(format, ap);
	if (ret < 0) {
		perror("fixdep");
		exit(1);
	}
	va_end(ap);
}

static void *read_file(const char *filename)
{
	struct stat st;
	int fd;
	char *buf;

	fd = open(filename, O_RDONLY);
	if (fd < 0) {
		fprintf(stderr, "fixdep: error opening file: ");  //输出错误信息到stderr流
		perror(filename); //在stderr流开头输出错误信息 filename:
		exit(2);  //退出程序,并返回2状态码
	}
	if (fstat(fd, &st) < 0) { // 读取文件的状态信息
		fprintf(stderr, "fixdep: error fstat'ing file: ");
		perror(filename);
		exit(2);
	}
	buf = malloc(st.st_size + 1); // 为buf分配内存,额外的 +1 字节通常用于存储字符串的终止符 \0,以确保缓冲区可以安全地用作字符串
	if (!buf) {
		perror("fixdep: malloc");
		exit(2);
	}
	if (read(fd, buf, st.st_size) != st.st_size) {
		perror("fixdep: read");
		exit(2);
	}
	buf[st.st_size] = '\0';
	close(fd);

	return buf;
}

/*
 * Important: The below generated source_foo.o and deps_foo.o variable
 * assignments are parsed not only by make, but also by the rather simple
 * parser in scripts/mod/sumversion.c.
 */
static void parse_dep_file(char *m, const char *target)
{
	char *p;
	int is_last, is_target;
	int saw_any_target = 0;
	int is_first_dep = 0;
	void *buf;

	while (1) {
		/* Skip any "white space" */
		while (*m == ' ' || *m == '\\' || *m == '\n')
			m++;

		if (!*m)
			break;

		/* Find next "white space" */
		p = m;
		while (*p && *p != ' ' && *p != '\\' && *p != '\n')
			p++;
		is_last = (*p == '\0');
		/* Is the token we found a target name? */
		is_target = (*(p-1) == ':');
		/* Don't write any target names into the dependency file */
		if (is_target) {
			/* The /next/ file is the first dependency */
			is_first_dep = 1;
		} else if (!is_ignored_file(m, p - m)) {
			*p = '\0';

			/*
			 * Do not list the source file as dependency, so that
			 * kbuild is not confused if a .c file is rewritten
			 * into .S or vice versa. Storing it in source_* is
			 * needed for modpost to compute srcversions.
			 */
      //第一次生成开头3 ~ 5行
			if (is_first_dep) {
				/*
				 * If processing the concatenation of multiple
				 * dependency files, only process the first
				 * target name, which will be the original
				 * source name, and ignore any other target
				 * names, which will be intermediate temporary
				 * files.
				 */
				if (!saw_any_target) {
					saw_any_target = 1;
          //第3行
					xprintf("source_%s := %s\n\n",
						target, m);
          //第5行开头
					xprintf("deps_%s := \\\n", target);
				}
				is_first_dep = 0;
			} else {
        //后续都是依赖文件导入
				xprintf("  %s \\\n", m);
			}

			buf = read_file(m);
      //例如输出:$(wildcard include/config/his/driver.h
      //会建立hash表,用于快速查找并不会重复导入
			parse_config_file(buf); //解析配置文件,并导入到cmd中
			free(buf);
		}

		if (is_last)
			break;

		/*
		 * Start searching for next token immediately after the first
		 * "whitespace" character that follows this token.
		 */
		m = p + 1;
	}

	if (!saw_any_target) {
		fprintf(stderr, "fixdep: parse error; no targets found\n");
		exit(1);
	}
  //生成末尾
	xprintf("\n%s: $(deps_%s)\n\n", target, target);
	xprintf("$(deps_%s):\n", target);
}

int main(int argc, char *argv[])
{
	const char *depfile, *target, *cmdline;
	void *buf;

	depfile = argv[1];
	target = argv[2];
	cmdline = argv[3];
  // 检查是否可以打印到.cmd文件,否则报错退出
	xprintf("cmd_%s := %s\n\n", target, cmdline);

	buf = read_file(depfile); //分配内存读取depfile文件,并返回指针
	parse_dep_file(buf, target);  //解析depfile文件并输出到cmd中
	free(buf);

	return 0;
}

scripts/Makefile.autoconf 用于生成配置文件

  1. 产生软链接 用于包含头文件
lrwxrwxrwx  1 embedsky embedsky     12 Jan  5 12:42 arch -> arch-stm32h7/
  1. 生成include/config.h文件 用于配置文件
/* Automatically generated - do not edit */
#define CFG_BOARDDIR board/st/stm32h750-art-pi
#include <configs/stm32h750-art-pi.h>
#include <asm/config.h>
#include <linux/kconfig.h>
#include <config_fallbacks.h>
  1. 生成u-boot.cfg文件 用于提取宏定义
#define CONFIG_ETH 1
#define CONFIG_CMD_FAT 1
#define CONFIG_TOOLS_SHA1 1
#define CONFIG_HAVE_TEXT_BASE 1
#define CONFIG_BOOTM_NETBSD 1
  1. 生成include/autoconf.mk文件 用于U-Boot常规配置
//空的
  1. 生成include/autoconf.mk.dep文件 用于生成自动配置文件的依赖关系
include/config/auto.conf: include/config.h include/linux/kconfig.h \
 include/generated/autoconf.h include/configs/stm32h750-art-pi.h \
 include/config.h arch/arm/include/asm/config.h include/linux/kconfig.h \
 include/config_fallbacks.h include/linux/sizes.h include/linux/const.h \
 include/config_distro_bootcmd.h

include/linux/kconfig.h:

include/generated/autoconf.h:

include/configs/stm32h750-art-pi.h:

include/config.h:

arch/arm/include/asm/config.h:

# 编译执行__all;依赖于include/autoconf.mk include/autoconf.mk.dep
__all: include/autoconf.mk include/autoconf.mk.dep
# 判定是否需要重新生成autoconf.mk
ifeq ($(shell grep -q '^CONFIG_SPL=y' include/config/auto.conf 2>/dev/null && echo y),y)
__all: spl/include/autoconf.mk
endif

ifeq ($(shell grep -q '^CONFIG_TPL=y' include/config/auto.conf 2>/dev/null && echo y),y)
__all: tpl/include/autoconf.mk
endif

ifeq ($(shell grep -q '^CONFIG_VPL=y' include/config/auto.conf 2>/dev/null && echo y),y)
__all: vpl/include/autoconf.mk
endif

# -MQ 指定生成的依赖文件的目标
# 生成include/autoconf.mk.dep,用于生成自动配置文件的依赖关系
quiet_cmd_autoconf_dep = GEN     $@
      cmd_autoconf_dep = $(CC) -x c -DDO_DEPS_ONLY -M -MP $(c_flags) \
	-MQ include/config/auto.conf include/config.h > $@ || {	\
		rm $@; false;							\
	}

include/autoconf.mk.dep: include/config.h FORCE
	$(call cmd,autoconf_dep)

# 我们正在一点一点地从 board headers 迁移到 Kconfig。
# 在此期间,我们同时使用
#  - include/config/auto.conf (由 Kconfig 生成)
#  - include/autoconf.mk (用于 U-Boot 常规配置)
# 以下规则创建 include/config/auto.conf autoconf.mk 以避免相同的 CONFIG 宏重复

# 使用 sed 处理输入文件,使用/tools/scripts/define2mk.sed的规则,将宏定义转换为 make 文件格式
# 使用 while 循环读取 sed 命令的输出,并逐行处理。read line 读取每一行,并将其存储在 line 变量中
# 如果 KCONFIG_IGNORE_DUPLICATES 变量存在,或者 auto.conf 文件中不存在包含 line 变量的行,则将 line 变量的内容输出到目标文件 $@
# 将处理后的输出重定向到目标文件 $@
quiet_cmd_autoconf = GEN     $@
      cmd_autoconf = \
		sed -n -f $(srctree)/tools/scripts/define2mk.sed $< |			\
		while read line; do							\
			if [ -n "${KCONFIG_IGNORE_DUPLICATES}" ] ||			\
			   ! grep -q "$${line%=*}=" include/config/auto.conf; then	\
				echo "$$line";						\
			fi;								\
		done > $@

# 依赖与u-boot.cfg
# 生成include/autoconf.mk(用于 U-Boot 常规配置)
include/autoconf.mk: u-boot.cfg
	$(call cmd,autoconf)

# -dM 选项用于生成宏定义列表
# -DDO_DEPS_ONLY 选项用于生成依赖关系
# $2 是一个占位符,表示传递给 cmd_autoconf 宏的第二个参数 即
# 使用 grep 命令从临时文件 $@.tmp 中提取包含 define CONFIG_ 的行。
# 使用 sed 命令删除包含 CONFIG_IS_ENABLED(、CONFIG_IF_ENABLED_INT( 和 CONFIG_VAL( 的行。
# 处理后的结果重定向到目标文件  u-boot.cfg
# 删除临时文件  u-boot.cfg.tmp
quiet_cmd_u_boot_cfg = CFG     $@
      cmd_u_boot_cfg = \
	$(CPP) $(c_flags) $2 -DDO_DEPS_ONLY -dM include/config.h > $@.tmp && { \
		grep 'define CONFIG_' $@.tmp | \
			sed '/define CONFIG_IS_ENABLED(/d;/define CONFIG_IF_ENABLED_INT(/d;/define CONFIG_VAL(/d;' > $@; \
		rm $@.tmp;						\
	} || {								\
		rm $@.tmp; false;					\
	}

# 依赖于include/config/auto.conf
u-boot.cfg: include/config.h FORCE
	$(call cmd,u_boot_cfg)

# include/config.h
# Prior to Kconfig, it was generated by mkconfig. Now it is created here.
define filechk_config_h
	(echo "/* Automatically generated - do not edit */";		\
	echo \#define CFG_BOARDDIR board/$(if $(VENDOR),$(VENDOR)/)$(BOARD);\
	$(if $(CONFIG_SYS_CONFIG_NAME),echo \#include \<configs/$(CONFIG_SYS_CONFIG_NAME).h\> ;) \
	echo \#include \<asm/config.h\>;				\
	echo \#include \<linux/kconfig.h\>;				\
	echo \#include \<config_fallbacks.h\>;)
endef
# 依赖于create_symlink
include/config.h: scripts/Makefile.autoconf create_symlink FORCE
# 执行filechk_config_h
	$(call filechk,config_h)

# 符号链接
# 如果存在 arch/$(ARCH)/mach-$(SOC)/include/mach,则创建指向该目录的符号链接。
# 否则,请创建一个指向 arch/$(ARCH)/include/asm/arch-$(SOC) 的符号链接。
PHONY += create_symlink
create_symlink:
# 这行代码检查目录 arch/$(ARCH)/mach-$(SOC)/include/mach 是否存在
# 如果目录存在,则将 dest 变量设置为相对路径 ../../mach-$(SOC)/include/mach。
# 否则,将 dest 变量设置为 arch-$(if $(SOC),$(SOC),$(CPU))。
# 创建符号链接从 arch/$(ARCH)/include/asm/arch 到 dest。
ifdef CONFIG_CREATE_ARCH_SYMLINK # 如果CONFIG_CREATE_ARCH_SYMLINK存在
	$(Q)if [ -d arch/$(ARCH)/mach-$(SOC)/include/mach ]; then	\
		dest=../../mach-$(SOC)/include/mach;			\
	else								\
		dest=arch-$(if $(SOC),$(SOC),$(CPU));			\
	fi;								\
	ln -fsn $$dest arch/$(ARCH)/include/asm/arch
endif
# 顶层Makefile
# Basic helpers built in scripts/
PHONY += scripts_basic
scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic
# $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=scripts/basic
	$(Q)rm -f .tmp_quiet_recordmcount
  • $(Q)$(MAKE) -f $(srctree)/scripts/Makefile.build obj=scripts/basic解析
  • 这是一种不指定目标的情况,由于未指定目标,这时会使用Makefile.build中的默认目标(第一个目标)__build。
  • __build在Makefile.build中的构建规则为:
# scripts/Makefile.build
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:

__build的变量解析与溯源

# scripts/Makefile.build
__build: $(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) \
	 $(if $(KBUILD_MODULES),$(obj-m) $(modorder-target)) \
	 $(subdir-ym) $(always)
	@:
  • 语句$(if $ (KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y))为空。
  • KBUILD_MODULES在顶层makefile中为空,$(if $(KBUILD_MODULES),$(obj-m) $(modorder-target))执行为空
  • 语句$(subdir-ym)为空

$(KBUILD_BUILTIN)

# 顶层Makefile
KBUILD_MODULES :=
KBUILD_BUILTIN := 1
include scripts/Kbuild.include
# scripts/Makefile.build
$(if $(KBUILD_BUILTIN),$(builtin-target) $(lib-target) $(extra-y)) 
# 由于KBUILD_BUILTIN = 1 展开为
$(builtin-target) $(lib-target) $(extra-y)

$(lib-target)

  • 因为都是空的,所以lib-target 为空 不会执行
#scripts/Makefile.build
lib-y :=
lib-m :=
#lib- 未定义
ifneq ($(strip $(lib-y) $(lib-m) $(lib-)),)
lib-target := $(obj)/lib.a
endif

$(builtin-target​​​​​​​)

  • 因为都是空的,所以builtin-target 为空 不会执行
# scripts/Makefile.build

obj-y :=
obj-m :=
# obj- 未定义
subdir-m :=
# lib-target为空
ifneq ($(strip $(obj-y) $(obj-m) $(obj-) $(subdir-m) $(lib-target)),)
builtin-target := $(obj)/built-in.o
endif

$(extra-y)

  • 未定义为空

$(subdir-ym)

  • scripts/Makefile.lib
# note:scripts/Makefile.lib
# Subdirectories we need to descend into
subdir-ym	:= $(sort $(subdir-y) $(subdir-m))
......
subdir-ym	:= $(addprefix $(obj)/,$(subdir-ym))
  • $(subdir-y) $(subdir-m)为空,subdir-ym为空

$(always) 重点关注

  • 在scripts/Makefile.build有如下定义
# note:scripts/Makefile.build
# The filename Kbuild has precedence over Makefile
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

src的定义

  • 在scripts/Makefile.build有如下定义
# note:scripts/Makefile.build
# Modified for U-Boot
prefix := vpl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
prefix := tpl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
prefix := spl
src := $(patsubst $(prefix)/%,%,$(obj))
ifeq ($(obj),$(src))
prefix := .
endif
endif
endif
  • 在命令make -f ./scripts/Makefile.build obj=scripts/basic我们传入了obj=scripts/basic
    所以src = obj = scripts/basic, prefix := .

setlocalversion 用于设置版本号

include/generated/version_autogenerated.h

  • 调用顺序:prepare(基本是所有步骤的前置条件)
    -> prepare0 -> archprepare -> prepare1 -> $(version_h)
    -> filechk_version.h ->生成include/generated/version_autogenerated.h
#顶层Makefile
# 将版本号写入include/generated/version_autogenerated.h
define filechk_version.h
	(echo \#define PLAIN_VERSION \"$(UBOOTRELEASE)\"; \
	echo \#define U_BOOT_VERSION \"U-Boot \" PLAIN_VERSION; \
	echo \#define U_BOOT_VERSION_NUM $(VERSION); \
	echo \#define U_BOOT_VERSION_NUM_PATCH $$(echo $(PATCHLEVEL) | \
		sed -e "s/^0*//"); \
	echo \#define HOST_ARCH $(HOST_ARCH); \
	echo \#define CC_VERSION_STRING \"$$(LC_ALL=C $(CC) --version | head -n 1)\"; \
	echo \#define LD_VERSION_STRING \"$$(LC_ALL=C $(LD) --version | head -n 1)\"; )
endef

$(version_h): include/config/uboot.release FORCE
	$(call filechk,version.h)
#define PLAIN_VERSION "2025.04-rc1"
#define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
#define U_BOOT_VERSION_NUM 2025
#define U_BOOT_VERSION_NUM_PATCH 4
#define HOST_ARCH 0x00a7
#define CC_VERSION_STRING "arm-none-eabi-gcc (15:9-2019-q4-0ubuntu1) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"
#define LD_VERSION_STRING "GNU ld (2.34-4ubuntu1+13ubuntu1) 2.34"

scripts/setlocalversion

  • makefile中的调用,设置版本号与调用的输入命令
# 顶层Makefile
# 手动修改的版本号
VERSION = 2025
PATCHLEVEL = 04
SUBLEVEL =
EXTRAVERSION = -rc1
NAME =
# 添加版本号的patch值与sub值
UBOOTVERSION = 	$(VERSION)
				# PATCHLEVEL不为空,在VERSION中添加.PATCHLEVEL
				$(if $(PATCHLEVEL) ,.$(PATCHLEVEL)
				# SUBLEVEL不为空,在VERSION中添加.SUBLEVEL
				$(if $(SUBLEVEL),.$(SUBLEVEL)))
				# 添加循环版本号
				$(EXTRAVERSION)

# 设置了KERNELVERSION变量=UBOOTVERSION,并调用了scripts/setlocalversion脚本,输入参数为srctree
define filechk_uboot.release
	KERNELVERSION=$(UBOOTVERSION) $(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree)
endef

PHONY += checkstack ubootrelease ubootversion
ubootrelease:
	@$(filechk_uboot.release)

@echo  '  ubootrelease	  - Output the release version string (use with make -s)'
  • 脚本scripts/setlocalversion的作用是设置版本号
# 寻找localversion*文件,将其中内容输出
collect_files()
{
	local file res=

	for file; do
		# 忽略临时文件
		case "$file" in
		*\~*)
			continue
			;;
		esac
		# 文件存在
		if test -e "$file"; then
			# 读取文件内容
			res="$res$(cat "$file")"
		fi
	done
	echo "$res"
}

scm_version()
{
	local short=false
	local no_dirty=false
	local tag
	# 获取输入参数并赋值
	while [ $# -gt 0 ];
	do
		case "$1" in
		--short)
			short=true;;
		--no-dirty)
			no_dirty=true;;
		esac
		shift
	done

	cd "$srctree"
	#检查当前是git根目录 
	if test -n "$(git rev-parse --show-cdup 2>/dev/null)"; then
		return
	fi
	# 检查是否有git
	if ! head=$(git rev-parse --verify HEAD 2>/dev/null); then
		return
	fi

	# x.x.0去掉0:  6.2.0-rc5  ->  v6.2-rc5
	# 没有的直接进行保留:    6.1.7      ->  v6.1.7
	# 匹配数字.数字 ,并将.0去掉,将后面的任意字符进行拼接
	version_tag=v$(echo "${KERNELVERSION}" | sed -E 's/^([0-9]+\.[0-9]+)\.0(.*)$/\1\2/')

	# 获取文件内的本地版本号
	tag=${file_localversion#-}
	desc=
	if [ -n "${tag}" ]; then	#文件内的本地版本号不为空
		# 如果tag存在,则获取tag的描述
		desc=$(git describe --match=$tag 2>/dev/null)
	fi
	# desc为空,且file_localversion不为空
	if [ -z "${desc}" ] && [ -n "${file_localversion}" ]; then
		tag="${version_tag}${file_localversion}"
		desc=$(git describe --match=$tag 2>/dev/null)
	fi

	# desc为空,默认为从 KERNELVERSION 派生的带注释的标记。
	if [ -z "${desc}" ]; then
		tag="${version_tag}"
		desc=$(git describe --match=$tag 2>/dev/null)
	fi

	# 如果当前提交是标记的提交,我们会忽略它,因为版本是定义明确。
	# 就是当前tag的记录上编译的话,不需要后面的commit id
	if [ "${tag}" != "${desc}" ]; then

		# 如果只要求提供简短版本,请不要运行更多 git 命令
		if $short; then
			echo "+"
			return
		fi
		# 如果我们超过了标记的提交,我们会漂亮地打印它。
		# (如 6.1.0-14595-g292a089d78d3)
		if [ -n "${desc}" ]; then
			#截取倒数第二个-后面的字符,格式化为5位整数
			echo "${desc}" | awk -F- '{printf("-%05d", $(NF-1))}'
		fi

		# Add -g and exactly 12 hex chars.
		printf '%s%s' -g "$(echo $head | cut -c1-12)"
	fi

	if ${no_dirty}; then
		return
	fi

	if {
		# 未暂存的更改
		git --no-optional-locks status -uno --porcelain 2>/dev/null ||
		# 已暂存未提交的更改
		git diff-index --name-only HEAD
	} | read dummy; then # 正在进行的更改
		printf '%s' -dirty
	fi
}

# 获取第一个参数是否为--no-local
if test "$1" = "--no-local"; then
	no_local=true	# 设置no_local为true
	shift			# 参数左移
fi

srctree=.
# 如果参数大于0,则将第一个参数赋值给srctree
if test $# -gt 0; then
	srctree=$1
	shift
fi
# 判定输入的目录是否存在
if test $# -gt 0 -o ! -d "$srctree"; then
	usage
fi

# 在根目录下匹配所有localversion开头的文件
file_localversion="$(collect_files localversion*)"
# 如果当前目录不是根目录,则在指定目录下匹配所有localversion开头的文件
if test ! "$srctree" -ef .; then
	file_localversion="${file_localversion}$(collect_files "$srctree"/localversion*)"
fi
# 如果no_local为true,则输出UBOOTVERSION
if ${no_local}; then
	echo "${KERNELVERSION}$(scm_version --no-dirty)"
	exit 0
fi
# 寻找include/config/auto.conf文件,没有该文件则证明还没有编译
if ! test -e include/config/auto.conf; then
	echo "Error: kernelrelease not valid - run 'make prepare' to update it" >&2
	exit 1
fi
# 从include/config/auto.conf文件中提取CONFIG_LOCALVERSION的值
config_localversion=$(sed -n 's/^CONFIG_LOCALVERSION=\(.*\)$/\1/p' include/config/auto.conf | tr -d '"')
# 在include/config/auto.conf文件中查找CONFIG_LOCALVERSION_AUTO是否使能
if grep -q "^CONFIG_LOCALVERSION_AUTO=y$" include/config/auto.conf; then
	scm_version="$(scm_version)"
elif [ "${LOCALVERSION+set}" != "set" ]; then	# 如果LOCALVERSION未设置
	scm_version="$(scm_version --short)"
fi

echo "${KERNELVERSION}${file_localversion}${config_localversion}${LOCALVERSION}${scm_version}"
  • 手动输入结果如下:
# 手动输入
$ KERNELVERSION=2025.04-rc1 ./scripts/setlocalversion .
2025.04-rc1-00007-ga2b489b170f8
# menuconfig中设置LOCALVERSION="mytest"
$ KERNELVERSION=2025.04-rc1 ./scripts/setlocalversion .
2025.04-rc1mytest-00007-ga2b489b170f8

# git 有变更后执行结果
$ KERNELVERSION=2025.04-rc1 ./scripts/setlocalversion .
2025.04-rc1mytest-00007-ga2b489b170f8-dirty
# 加入--no-local参数的结果
$ KERNELVERSION=2025.04-rc1 ./scripts/setlocalversion --no-local .
2025.04-rc1-00007-ga2b489b170f8

# 去掉LOCALVERSION_AUTO=y的结果
$ KERNELVERSION=2025.04-rc1 ./scripts/setlocalversion .
2025.04-rc1+

# 根目录下创建localversion文件,写入myfile
$ $ echo "myfile" > localversion;KERNELVERSION=2025.04-rc1 ./scripts/setloc
alversion .
2025.04-rc1myfile-00007-ga2b489b170f8
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值