从零开始逐步实现U-Boot(一)——Kconfig/Kbuild配置构建系统搭建与主机程序编译

系列:从零开始逐步实现U-Boot

目标平台:NXP i.MX6ULL (ARM Cortex-A7)

QEMU mcimx6ul-evk模拟板 或 正点原子IMX6ULL-MINI板

U-Boot(Universal Bootloader)作为嵌入式系统的核心组件,其构建系统的演进经历了从早期的纯 Makefile 到引入 Linux 内核式 Kconfig/Kbuild 系统的转变。这一转变极大地增强了代码的可配置性和模块化。

本篇将聚焦于 Kconfig 与 Kbuild 基础架构 的简化版本搭建,这是实现可视化配置(make menuconfig)和自动化编译的基础。

项目源码地址:uscxi/imx6ull-custom-uboot: 从零开始逐步实现U-Boot

1. 实验环境准备

  • 宿主机: Ubuntu 24.04 LTS
  • 工具链: gcc, make, flex, bison等
  • 交叉编译工具链:gcc-arm-none-eabi(10.3.1 20210824)
  • 目标框架: 仿照 U-Boot v2025.x+ 结构
  • 目标机器:(可在下面两个目标上运行,推荐虚拟机器)
    • QEMU 上的虚拟机器 mcimx6ul-evk(Freescale i.MX6UL Evaluation Kit (Cortex-A7))
    • 正点原子IMX6ULL-Mini开发板

2. 核心目录结构

首先,我们需要建立最基础的目录树,剥离 U-Boot 的繁杂源码,只保留构建系统核心。

u-boot/
├── Kconfig             # 顶层配置入口
├── Makefile            # 顶层 Makefile,处理全局逻辑
├── config.mk/          # Makefile 相关变量
├── arch/               # 架构相关代码(存放 Kconfig)
│   └── arm/
├── scripts/            # Kconfig/Kbuild 核心脚本
│   ├── basic/          # 构建工具 fixdep 源码
│   ├── kconfig/        # Kconfig 配置工具源码
│   ├── dtc/            # 设备树工具 DTC 源码
│   ├── gcc-version.sh  # 相关脚本
│   ├── setlocalversion # 相关脚本
│   ├── mkmakefile      # 外部构建目录中生成辅助 Makefile
│   ├── Makefile        # 主机程序编译 Makefile
│   ├── Kbuild.include  # 定义通用 Makefile 函数
│   ├── Kbuild.lib      # Kbuild 文件中的对象列表处理
│   ├── Kbuild.host     # 用于编译主机程序(如mkimage)的规则
│   ├── Kbuild.autoconf # 处理自动生成的配置头文件
│   ├── Kbuild.clean    # 清理规则
│   ├── Kconfig.include # Kconfig 配置工具变量
│   └── Makefile.build  # 递归编译的核心脚本
├── include/            # 头文件
└── configs/            # 存放默认配置文件(defconfig)

3. 顶层 Makefile 实现

顶层 Makefile 是整个构建系统的入口,负责环境变量初始化、Kconfig 工具链调用以及编译流程控制。

基本配置与输出美化控制

# 最终版本号格式: VERSION.PATCHLEVEL.SUBLEVEL-EXTRAVERSION
# 例如: 2025.04-rc3
VERSION = 2025 # 主版本号,通常与发布年份对应
PATCHLEVEL = 04 # 补丁级别,通常与发布月份对应(例如04代表四月)
SUBLEVEL =      # 子版本号,表示当前版本的第几次修正
EXTRAVERSION =  # 额外版本标识,用于标记候选版本(如 -rc1、-rc2)或自定义后缀
NAME =          # 版本代号,可填写该版本的昵称,通常用于开发者内部标识,通常留空

# 禁用内置规则和内置变量
# 确保所有工具链变量都由本 Makefile 显式定义,避免环境污染,便于理解和维护。
MAKEFLAGS += -rR

# 确定宿主机的架构
# include/host_arch.h 定义了架构常量,形如:
#   #define HOST_ARCH_X86       0x0386
#   #define HOST_ARCH_X86_64    0x8664
#   #define HOST_ARCH_ARM       0x00a7
#   #define HOST_ARCH_AARCH64   0xaa64
#   等等...
include include/host_arch.h
ifeq ("", "$(CROSS_COMPILE)") # 如果没有设置交叉编译工具链(本地编译)
  MK_ARCH="${shell uname -m}" # 使用 uname -m 获取本机架构,例如 x86_64
else # 否则,已设置交叉编译工具链
  MK_ARCH="${shell echo $(CROSS_COMPILE) | sed -n 's/^\(.*ccache\)\{0,1\}[[:space:]]*\([^\/]*\/\)*\([^-]*\)-[^[:space:]]*/\3/p'}" # 从工具链前缀中提取目标架构(如 aarch64-linux-gnu- → aarch64),并处理可能的 ccache 前缀
endif
unexport HOST_ARCH
ifeq ("x86_64", $(MK_ARCH))
  export HOST_ARCH=$(HOST_ARCH_X86_64)
else ifneq (,$(findstring $(MK_ARCH), "aarch64" "armv8l"))
  export HOST_ARCH=$(HOST_ARCH_AARCH64)
else ifneq (,$(findstring $(MK_ARCH), "arm" "armv7" "armv7a" "armv7l"))
  export HOST_ARCH=$(HOST_ARCH_ARM)
endif
undefine MK_ARCH # 清理临时变量 MK_ARCH

# Locale 环境隔离
# 构建系统必须在确定性的语言环境下运行。
unexport LC_ALL # 取消 LC_ALL 环境变量,避免覆盖具体分类设置
LC_COLLATE=C # 设置字符串排序规则为 C(简单 ASCII 顺序),保证构建输出一致性
LC_NUMERIC=C # 设置数字格式为 C,避免因区域不同导致小数点等问题
export LC_COLLATE LC_NUMERIC # 导出这两个设置,使其在子进程中生效

# Avoid interference with shell env settings
unexport GREP_OPTIONS

# 输出美化控制
#   V=0(默认): 精简输出
#     quiet = "quiet_"
#     Q     = "@"
#     效果: 只显示 "CC xxx.o" 等摘要信息
#
#   V=1(详细): 完整命令输出
#     quiet = ""
#     Q     = ""
#     效果: 显示完整的编译命令行
#
#   make -s(静默): 几乎无输出
#     quiet = "silent_"
#     效果: 抑制所有非错误输出
ifeq ("$(origin V)", "command line")
  KBUILD_VERBOSE = $(V)
endif
ifndef KBUILD_VERBOSE
  KBUILD_VERBOSE = 0
endif

ifeq ($(KBUILD_VERBOSE),1)
  quiet =
  Q =
else
  quiet=quiet_
  Q = @
endif

# 检测 make -s(静默模式)的兼容性处理
# make 4.x 和 3.8x 在 MAKEFLAGS 中表示 -s 的方式不同:
#   make 4.x: MAKEFLAGS 第一个词中包含 's',且前面可能有其他单字符选项
#   make 3.8x: MAKEFLAGS 以 "s" 开头或包含 "-s"
ifneq ($(filter 4.%,$(MAKE_VERSION)),)	# make-4
ifneq ($(filter %s ,$(firstword x$(MAKEFLAGS))),)
  quiet=silent_
endif
else					# make-3.8x
ifneq ($(filter s% -s%,$(MAKEFLAGS)),)
  quiet=silent_
endif
endif

# 导出到所有子 make 进程,确保递归构建中输出行为一致
export quiet Q KBUILD_VERBOSE

外部构建(Out-of-tree Build)支持

U-Boot/Linux 支持将构建产物放到独立目录(O=

),保持源码目录干净。实现机制:

make O=./build

第一次 make 在源码目录启动,设置 KBUILD_OUTPUT=./build,在./build 中启动第二次 make,KBUILD_SRC 指向源码目录,第二次 make 完成实际构建。

判断流程:

  • KBUILD_SRC 为空?第一次调用在源码目录中,检查 O 变量, 如果有,启动 sub-make;

  • KBUILD_SRC 非空?第二次调用在输出目录中,跳过此段,进入实际构建逻辑。

ifeq ($(KBUILD_SRC),)

# 检查用户是否通过命令行指定了输出目录
# origin 函数确保只接受命令行参数,不接受环境变量
ifeq ("$(origin O)", "command line")
  KBUILD_OUTPUT := $(O)
endif

# _all 是未指定任何目标时的默认目标
# PHONY 变量收集所有伪目标,最后统一声明 .PHONY
PHONY := _all
_all:

# 防止 make 尝试用隐式规则重新生成 Makefile 自身
$(CURDIR)/Makefile Makefile: ;

ifneq ($(KBUILD_OUTPUT),)
# 创建输出目录并解析为绝对路径
saved-output := $(KBUILD_OUTPUT)
KBUILD_OUTPUT := $(shell mkdir -p $(KBUILD_OUTPUT) && cd $(KBUILD_OUTPUT) \

# 如果 KBUILD_OUTPUT 为空,说明目录创建失败&& /bin/pwd)
$(if $(KBUILD_OUTPUT),, \
     $(error failed to create output directory "$(saved-output)"))

# 让 make 在查找 include 文件时也搜索源码根目录
# 这使得 "include scripts/Kbuild.include" 等语句在输出目录中也能工作
MAKEFLAGS += --include-dir=$(CURDIR)

PHONY += $(MAKECMDGOALS) sub-make

# 将所有目标(除了 _all, sub-make, 和 Makefile 本身)重定向到 sub-make
# @: 是空命令(冒号是 shell 的 no-op),表示"什么都不做"
# 实际工作全部由 sub-make 完成
$(filter-out _all sub-make $(CURDIR)/Makefile, $(MAKECMDGOALS)) _all: sub-make
	@:

# sub-make:在输出目录中启动第二次 make
#   -C $(KBUILD_OUTPUT):切换到输出目录
#   KBUILD_SRC=$(CURDIR):告诉子 make 源码在哪里
#   -f $(CURDIR)/Makefile:使用源码目录的 Makefile
#   filter-out:过滤掉内部目标,只传递用户指定的目标
sub-make: FORCE
	$(Q)$(MAKE) -C $(KBUILD_OUTPUT) KBUILD_SRC=$(CURDIR) \
	-f $(CURDIR)/Makefile $(filter-out _all sub-make,$(MAKECMDGOALS))

# 设置跳过标志,后续所有构建逻辑都不再执行
# (因为实际构建已委托给 sub-make)
skip-makefile := 1
endif # ifneq ($(KBUILD_OUTPUT),)
endif # ifeq ($(KBUILD_SRC),)

# 只有当 skip-makefile 未设置时,才继续处理后续内容。
# 此时我们要么在源码目录中直接构建(无 O=),
# 要么在输出目录中作为 sub-make 运行。
ifeq ($(skip-makefile),)

# 抑制 "Entering directory ..." 消息
# 进入输出目录时的消息由 sub-make 机制自行处理
# 这里抑制的是后续递归进入子目录时的消息
MAKEFLAGS += --no-print-directory

# 确保 _all(默认目标)最终指向 all
PHONY += all
_all: all

源码树定位

根据是否为外部构建,确定 srctree 的值:

  • 原地构建(KBUILD_SRC 为空): srctree = .

  • 在源码子目录构建: srctree = ..

  • 完全外部构建: srctree = $(KBUILD_SRC) (绝对路径)

ifeq ($(KBUILD_SRC),)
        # building in the source tree
        srctree := .
else
        ifeq ($(KBUILD_SRC)/,$(dir $(CURDIR)))
                # building in a subdirectory of the source tree
                srctree := ..
        else
                srctree := $(KBUILD_SRC)
        endif
endif
objtree		:= . # 构建产物总是在当前目录
src		:= $(srctree)
obj		:= $(objtree)

# 当 make 在当前目录找不到依赖文件时,会自动到 VPATH 指定的目录中查找
VPATH		:= $(srctree)

export srctree objtree VPATH

# CDPATH 是 bash 的环境变量,会影响 cd 命令的行为
# 在 Makefile 中执行 shell 命令时,CDPATH 可能导致 cd 跳转到意外目录
unexport CDPATH

宿主机环境探测

检测宿主机操作系统,用于后续条件判断的特殊编译选项,编译主机程序。

# 探测宿主机 CPU 架构,统一名称。
HOSTARCH := $(shell uname -m | \
	sed -e s/i.86/x86/ \
	    -e s/sun4u/sparc64/ \
	    -e s/arm.*/arm/ \
	    -e s/sa110/arm/ \
	    -e s/ppc64/powerpc/ \
	    -e s/ppc/powerpc/ \
	    -e s/macppc/powerpc/\
	    -e s/sh.*/sh/)
# 探测宿主机操作系统
HOSTOS := $(shell uname -s | tr '[:upper:]' '[:lower:]' | \
	    sed -e 's/\(cygwin\).*/cygwin/')

export	HOSTARCH HOSTOS

交叉编译与基础配置

如果宿主机架构与目标架构相同,表示这是原生编译,此时无需交叉编译器前缀。

# ?= 表示仅在变量未定义时才赋值,允许用户覆盖
ifeq ($(HOSTARCH),$(ARCH))
CROSS_COMPILE ?=
endif

# .config 文件路径,Kconfig 系统的核心输出
# 用户可以通过 KCONFIG_CONFIG 环境变量指定替代路径
# 默认为源码根目录下的 .config
KCONFIG_CONFIG	?= .config
export KCONFIG_CONFIG

# 宿主机工具链配置
# CONFIG_SHELL:Kbuild 使用的 shell 解释器
# 优先使用 bash(因为部分 Kbuild 脚本依赖 bash 特性),
# 降级为 /bin/bash,最终降级为 sh
CONFIG_SHELL := $(shell if [ -x "$$BASH" ]; then echo $$BASH; \
	  else if [ -x /bin/bash ]; then echo /bin/bash; \
	  else echo sh; fi ; fi)

# 宿主机编译器(用于编译构建过程中在宿主机上运行的工具)
# 注意区分:
#   HOSTCC     — 编译宿主机工具(如 scripts/kconfig/mconf)
#   CC         — 编译目标平台代码(U-Boot 本身)
HOSTCC       = cc                      # 系统默认 C 编译器
HOSTCXX      = c++                     # 系统默认 C++ 编译器

# 宿主机编译标志
# -Wall: 开启所有常见警告
# -Wstrict-prototypes: 要求函数原型声明中必须有参数列表
# -O2: 开启优化(宿主机工具不需要调试,追求速度)
# -fomit-frame-pointer: 省略帧指针,释放一个寄存器
KBUILD_HOSTCFLAGS   := -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer \
		$(HOSTCFLAGS)
KBUILD_HOSTCXXFLAGS := -O2 $(HOSTCXXFLAGS)
KBUILD_HOSTLDFLAGS  := $(HOSTLDFLAGS)
KBUILD_HOSTLDLIBS   := $(HOSTLDLIBS)

# C 语言标准:使用 GNU C11(C11 + GNU 扩展)
# GNU 扩展包括:内联汇编、语句表达式、typeof、__attribute__ 等
CSTD_FLAG := -std=gnu11
KBUILD_HOSTCFLAGS += $(CSTD_FLAG)

export KBUILD_SRC

Kbuild 核心框架

Kbuild.include 是整个构建系统的”标准库”,它定义了大量核心函数和变量。

# 空规则 "scripts/Kbuild.include: ;" 防止 make 尝试重新生成此文件
scripts/Kbuild.include: ;
include scripts/Kbuild.include

# 目标平台工具链定义
# $(CROSS_COMPILE) 是交叉编译器前缀,例如:
#   arm-linux-gnueabihf-  → AS = arm-linux-gnueabihf-as
#   aarch64-none-elf-     → CC = aarch64-none-elf-gcc

AS		= $(CROSS_COMPILE)as           # 汇编器

# 链接器:优先使用 ld.bfd
# 原因:
#   1) BFD (Binary File Descriptor) 链接器是 GNU ld 的传统实现
#   2) gold 链接器虽然更快,但对某些嵌入式链接脚本的支持不完整
#   3) LLVM 的 lld 在裸机(bare-metal)场景下可能有兼容性问题
#   4) U-Boot 的链接脚本依赖 BFD ld 的某些特定行为
ifneq ($(shell $(CROSS_COMPILE)ld.bfd -v 2> /dev/null),)
LD		= $(CROSS_COMPILE)ld.bfd
else
LD		= $(CROSS_COMPILE)ld
endif

CC		= $(CROSS_COMPILE)gcc          # C 编译器
CPP		= $(CC) -E                     # C 预处理器(仅预处理,不编译)
AR		= $(CROSS_COMPILE)ar           # 静态库打包工具
NM		= $(CROSS_COMPILE)nm           # 符号表查看工具
LDR		= $(CROSS_COMPILE)ldr          # U-Boot 特有的镜像加载工具
STRIP		= $(CROSS_COMPILE)strip        # 符号剥离工具
OBJCOPY		= $(CROSS_COMPILE)objcopy      # 目标文件格式转换(ELF→BIN 等)
OBJDUMP		= $(CROSS_COMPILE)objdump      # 反汇编工具
LEX		= flex                         # 词法分析器生成器(用于 Kconfig 解析)
YACC		= bison                        # 语法分析器生成器(用于 Kconfig 解析)

# 设备树编译器(DTC)配置
# DTC (Device Tree Compiler) 用于编译 .dts → .dtb
# U-Boot 自带 DTC 源码(scripts/dtc/),默认使用内建版本
# 用户可通过 DTC=/usr/bin/dtc 指定外部版本
# 如果使用外部 DTC,假设 pylibfdt(Python 绑定)也可用
DTC_INTREE	:= $(objtree)/scripts/dtc/dtc # 内建 DTC 路径
DTC		?= $(DTC_INTREE)               # 默认使用内建版本
DTC_MIN_VERSION	:= 010406                      # 最低版本要求:1.4.6

编译标志定义

# C 预处理器标志
# -D__KERNEL__: 标识正在编译内核态代码(U-Boot 复用了大量 Linux 内核头文件)
# -D__UBOOT__:  标识正在编译 U-Boot(区分于 Linux 内核和其他项目)
KBUILD_CPPFLAGS := -D__KERNEL__ -D__UBOOT__

# C 编译器标志
# -Wall:              开启大部分编译警告
# -Wstrict-prototypes: 要求严格的函数原型声明
# -Wno-format-security: 抑制 printf 格式字符串安全警告
#                       (U-Boot 中大量使用运行时动态格式字符串)
# -fno-builtin:       禁用 GCC 内建函数(如 memcpy, printf 的优化替换)
#                     U-Boot 有自己的实现,不能被编译器替换
# -ffreestanding:     告知编译器这是独立环境(非宿主环境)
#                     不假设标准库和启动代码的存在
# -fshort-wchar:      wchar_t 使用 2 字节(与 UEFI 规范一致)
# -fno-strict-aliasing: 禁用严格别名规则优化
#                       嵌入式代码中经常需要通过不同类型指针访问同一内存
KBUILD_CFLAGS   := -Wall -Wstrict-prototypes \
		   -Wno-format-security \
		   -fno-builtin -ffreestanding $(CSTD_FLAG)
KBUILD_CFLAGS	+= -fshort-wchar -fno-strict-aliasing

# 汇编器标志
# -D__ASSEMBLY__: 在汇编文件中定义此宏
#   使得 C 头文件在被汇编文件 include 时可以条件编译掉 C 专有内容
KBUILD_AFLAGS   := -D__ASSEMBLY__

# 链接器标志(此处为空,由架构 Makefile 和 board 配置追加)
KBUILD_LDFLAGS  :=

# 禁止生成位置无关代码(PIE - Position Independent Executable)
# 原因:
#   1) U-Boot 有自己的重定位机制,不依赖 PIE
#   2) PIE 会引入 GOT/PLT 等开销,不适合 bootloader
#   3) 某些现代 GCC 默认开启 PIE,需要显式关闭
# cc-option 是 Kbuild.include 中定义的探测函数:
#   如果编译器支持 -fno-PIE 则使用之,否则忽略
KBUILD_CFLAGS	+= $(call cc-option,-fno-PIE)
KBUILD_AFLAGS	+= $(call cc-option,-fno-PIE)

# 版本字符串生成
# UBOOTRELEASE: 完整版本字符串,从 include/config/uboot.release 读取
#   该文件由 scripts/setlocalversion 生成
#   内容示例: "2025.04-rc3-00001-gabcdef1234-dirty"
#   包含: 版本号 + git 信息 + 脏标记
# UBOOTVERSION: 基础版本字符串(不含 git 信息)
#   内容示例: "2025.04-rc3"
UBOOTRELEASE = $(shell cat include/config/uboot.release 2> /dev/null)
UBOOTVERSION = $(VERSION)$(if $(PATCHLEVEL),.$(PATCHLEVEL)$(if $(SUBLEVEL),.$(SUBLEVEL)))$(EXTRAVERSION)

# 将所有关键变量导出到子 make 进程和 shell 命令
# 按功能分组:
#   版本信息 → 子 Makefile 和链接脚本中使用
#   架构信息 → 决定编译哪些目录和文件
#   工具链   → 所有编译命令都需要
#   编译标志 → 统一的编译选项
export VERSION PATCHLEVEL SUBLEVEL UBOOTRELEASE UBOOTVERSION
export ARCH CPU BOARD VENDOR SOC CPUDIR BOARDDIR
export CONFIG_SHELL HOSTCC KBUILD_HOSTCFLAGS CROSS_COMPILE AS LD CC
export CPP AR NM LDR STRIP OBJCOPY OBJDUMP KBUILD_HOSTLDFLAGS KBUILD_HOSTLDLIBS
export LEX YACC DTC
export KBUILD_CPPFLAGS KBUILD_CFLAGS KBUILD_AFLAGS
export UBOOTINCLUDE KBUILD_LDFLAGS

export CC_VERSION_TEXT := $(shell $(CC) --version | head -n 1)

export RCS_FIND_IGNORE := \( -name SCCS -o -name BitKeeper -o -name .svn -o    \
			  -name CVS -o -name .pc -o -name .hg -o -name .git \) \
			  -prune -o

配置目标与构建目标共享的规则

scripts_basic:编译最基础的宿主机工具。

scripts/basic/ 下通常包含 fixdep 工具,这个工具是 Kconfig 等后续步骤的前置依赖:

fixdep:修正 GCC 生成的依赖文件(.d),使其与 Kbuild 的依赖跟踪系统兼容。它将 #include 的头文件依赖转化为CONFIG_xxx 的依赖,这样只有相关配置改变时才重编译。

# $(build) 在 Kbuild.include 中定义为:
#   build := -f $(srctree)/scripts/Makefile.build obj
# 因此 $(Q)$(MAKE) $(build)=scripts/basic 展开为:
#   @make -f scripts/Makefile.build obj=scripts/basic
PHONY += scripts_basic
scripts_basic:
	$(Q)$(MAKE) $(build)=scripts/basic

# 防止 scripts/basic/ 下的文件触发隐式规则
# 空规则确保 make 不会尝试"构建"这些文件
scripts/basic/%: scripts_basic ;

外部构建目录中生成辅助 Makefile

# outputmakefile: 在外部构建目录中生成辅助 Makefile
# 这样用户可以在输出目录中直接运行 make,无需指定 -f
PHONY += outputmakefile
outputmakefile:
ifneq ($(KBUILD_SRC),)
	$(Q)ln -fsn $(srctree) source      # 创建 source → 源码目录的符号链接
	$(Q)$(CONFIG_SHELL) $(srctree)/scripts/mkmakefile $(srctree)
endif

目标分类

Kbuild 将 make 目标分为三类:

  1. 配置目标(*config): menuconfig,defconfig,oldconfig 等
  2. 非配置目标: all,u-boot.bin,clean 等
  3. 混合目标:同时指定配置和非配置目标(如 make defconfig all)

分类的原因:

配置目标不需要读取 .config(它们的目的就是生成 .config),构建目标需要读取 .config(否则不知道编译什么),混合目标需要逐个执行(先配置,再构建)。

# 版本和时间戳头文件路径
# 这些文件在 prepare 阶段生成,供 C 代码引用
version_h := include/generated/version_autogenerated.h
timestamp_h := include/generated/timestamp_autogenerated.h

# 不需要 .config 的目标列表
# clean/mrproper/distclean: 清理目标
# ubootversion: 只打印版本号
no-dot-config-targets := clean mrproper distclean \
			 ubootversion

# 三个控制标志的初始值
config-targets := 0    # 是否包含配置目标
mixed-targets  := 0    # 是否同时包含配置和非配置目标
dot-config     := 1    # 是否需要读取 .config

# 检查当前目标是否全部为"不需要 .config"的目标
# 逻辑:
#   如果 MAKECMDGOALS 中有 no-dot-config-targets 的成员,则继续检查
#     如果过滤掉这些目标后为空,全部是不需要 .config 的,则dot-config := 0
#     否则,还有其他目标需要 .config,保持 dot-config := 1
ifneq ($(filter $(no-dot-config-targets), $(MAKECMDGOALS)),)
	ifeq ($(filter-out $(no-dot-config-targets), $(MAKECMDGOALS)),)
		dot-config := 0
	endif
endif

# 检查是否包含配置目标
# "config" 匹配纯 "config" 目标
# "%config" 匹配 menuconfig, defconfig, oldconfig 等
ifneq ($(filter config %config,$(MAKECMDGOALS)),)
        config-targets := 1
        # 如果目标多于 1 个(即除了 *config 还有其他),则为混合目标
        ifneq ($(words $(MAKECMDGOALS)),1)
                mixed-targets := 1
        endif
endif

# 混合目标处理
ifeq ($(mixed-targets),1)
# 当用户执行类似 "make defconfig all" 时:
# 不能同时处理配置和构建,因为 .config 必须先生成
# 解决方案:逐个目标递归调用 make
#   1. make -f Makefile defconfig
#   2. make -f Makefile all

PHONY += $(MAKECMDGOALS) __build_one_by_one

# 除 __build_one_by_one 外的所有目标都依赖它
# @: 空命令——这些目标本身不做任何事,实际工作在 __build_one_by_one 中
$(filter-out __build_one_by_one, $(MAKECMDGOALS)): __build_one_by_one
	@:

# 核心逻辑:用 set -e 确保任一步骤失败立即停止
# 然后 for 循环逐个调用 make
__build_one_by_one:
	$(Q)set -e; \
	for i in $(MAKECMDGOALS); do \
		$(MAKE) -f $(srctree)/Makefile $$i; \
	done

else # mixed-targets=0

# 配置目标处理
ifeq ($(config-targets),1)

# KBUILD_DEFCONFIG: 默认的 defconfig 文件
KBUILD_DEFCONFIG := 
export KBUILD_DEFCONFIG KBUILD_KCONFIG

# config 和 %config 规则:
# 依赖:
#   scripts_basic — 确保 fixdep 等基础工具已编译
#   outputmakefile — 确保外部构建的 Makefile 已生成
#   FORCE — 强制每次都执行(即使目标文件存在)
#
# 动作:
#   进入 scripts/kconfig 目录执行对应目标
#   scripts/kconfig/Makefile 会编译并运行:
#     conf  — 命令行配置工具(处理 defconfig, oldconfig, syncconfig)
#     mconf — ncurses 图形界面(处理 menuconfig)
#     nconf — 更现代的 ncurses 界面(处理 nconfig)
#
# 例如 make menuconfig 展开为:
#   @make -f scripts/Makefile.build obj=scripts/kconfig menuconfig
config: scripts_basic outputmakefile FORCE
	$(Q)$(MAKE) $(build)=scripts/kconfig $@

%config: scripts_basic outputmakefile FORCE
	$(Q)$(MAKE) $(build)=scripts/kconfig $@

else # config-targets=0

# 构建目标处理
# 处理所有非配置目标:u-boot.bin、all、clean 等
# include/config.mk 由 Kconfig 的 syncconfig 生成
# -include: 文件不存在时不报错(首次构建时该文件尚未生成)
-include include/config.mk

# scripts 目标:编译辅助脚本工具
# 仅在 auto.conf 存在后才编译,确保配置已完成
PHONY += scripts
scripts: scripts_basic scripts_dtc include/config/auto.conf
	$(Q)$(MAKE) $(build)=$(@)

# .config 与 auto.conf 同步机制
ifeq ($(dot-config),1)

# 读取 auto.conf
# 这些变量在后续 Makefile 逻辑中作为条件判断使用
-include include/config/auto.conf

# auto.conf.cmd 记录了生成 auto.conf 时的 Kconfig 依赖
# 这样当任何 Kconfig 文件改变时,auto.conf 会自动重新生成
-include include/config/auto.conf.cmd

# 防止 make 尝试用隐式规则重建这些文件
$(KCONFIG_CONFIG) include/config/auto.conf.cmd: ;

自动配置同步规则

触发条件(任一):

  1. .config 比 auto.conf 新(用户手动编辑了 .config)
  2. auto.conf.cmd 不存在(首次构建或 clean 后)
  3. 某个 Kconfig 文件被修改(通过 auto.conf.cmd 的依赖跟踪)

执行流程:
Step 1: make syncconfig
运行 scripts/kconfig/conf –syncconfig Kconfig
读取 .config,与 Kconfig 树校验,生成:

  • include/config/auto.conf (Makefile 变量)

  • include/config/auto.conf.cmd (依赖追踪)

  • include/generated/autoconf.h (C 宏定义)

  • include/config/ 下的空文件 (精细依赖追踪)

Step 2: make -f scripts/Makefile.autoconf
生成 include/autoconf.mk 等兼容性文件
如果失败,删除 auto.conf 强制下次重新生成

Step 3: touch auto.conf
更新时间戳,确保 auto.conf 比 autoconf.h 新
避免 syncconfig 被重复触发

include/config/%.conf: $(KCONFIG_CONFIG) include/config/auto.conf.cmd
	$(Q)$(MAKE) -f $(srctree)/Makefile syncconfig
	@# 如果 Makefile.autoconf 执行失败,删除 auto.conf
	@# 这样下次 make 时会重新触发 syncconfig
	$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf || \
		{ rm -f include/config/auto.conf; false; }
	@# 更新 auto.conf 的时间戳
	@# 防止 auto.conf 比 autoconf.h 旧导致循环触发
	$(Q)touch include/config/auto.conf

# u-boot.cfg: 生成完整的预处理配置文件
# 用于调试——可以看到所有 CONFIG_xxx 的最终值
u-boot.cfg:
	$(Q)$(MAKE) -f $(srctree)/scripts/Makefile.autoconf $(@)

# 旧式兼容性配置文件
# autoconf.mk: 类似 auto.conf 但格式略有不同
# autoconf.mk.dep: autoconf.mk 的依赖文件
-include include/autoconf.mk
-include include/autoconf.mk.dep

# 三重条件守卫确保只在配置完全就绪时才加载架构 Makefile:
#   1) .config 文件存在
#   2) auto.conf 文件存在
#   3) auto.conf 不比 .config 旧(用户没有在生成后修改 .config)
#
# config.mk: 传统的板级配置(ARCH, CPU, BOARD 等变量)
# arch/$(ARCH)/Makefile: 架构级编译规则
#   定义 head-y(启动代码), PLATFORM_CPPFLAGS 等关键变量
ifneq ($(wildcard $(KCONFIG_CONFIG)),)
ifneq ($(wildcard include/config/auto.conf),)
autoconf_is_old := $(shell find . -path ./$(KCONFIG_CONFIG) -newer \
						include/config/auto.conf)
ifeq ($(autoconf_is_old),)
include config.mk
include arch/$(ARCH)/Makefile
endif
endif
endif

链接脚本

链接脚本(Linker Script,.lds)决定了二进制文件的内存布局。

U-Boot 的链接脚本通常定义:

  • 代码段、数据段的起始地址

  • 特殊段(如 .u_boot_list 用于命令/驱动注册)

  • 堆栈位置

搜索优先级:

  1. CONFIG_SYS_LDSCRIPT 显式指定(Kconfig 中配置)
  2. board/$(BOARDDIR)/u-boot.lds(板级定制)
  3. $(CPUDIR)/u-boot.lds(CPU 级别)
  4. arch/$(ARCH)/cpu/u-boot.lds(架构级别通用)
ifndef LDSCRIPT
	ifdef CONFIG_SYS_LDSCRIPT
		# 去除 Kconfig 字符串值的双引号
		# Kconfig 中 string 类型的值带引号: CONFIG_SYS_LDSCRIPT="arch/arm/cpu/u-boot.lds"
		LDSCRIPT := $(srctree)/$(CONFIG_SYS_LDSCRIPT:"%"=%)
	endif
endif

ifndef LDSCRIPT
	ifeq ($(wildcard $(LDSCRIPT)),)
		LDSCRIPT := $(srctree)/board/$(BOARDDIR)/u-boot.lds
	endif
	ifeq ($(wildcard $(LDSCRIPT)),)
		LDSCRIPT := $(srctree)/$(CPUDIR)/u-boot.lds
	endif
	ifeq ($(wildcard $(LDSCRIPT)),)
		LDSCRIPT := $(srctree)/arch/$(ARCH)/cpu/u-boot.lds
	endif
endif

else #dot-config=0

# dot-config=0 时(clean 等目标),auto.conf 不需要存在
# 提供空规则防止 make 报错
include/config/auto.conf: ;

endif # $(dot-config)

优化级别控制

优化级别通过 Kconfig 配置,而非在 Makefile 中硬编码,这样用户可以通过 menuconfig 灵活切换。

ifdef CONFIG_CC_OPTIMIZE_FOR_SIZE
# -Os: 优化代码体积(bootloader 通常受 ROM/Flash 大小限制)
# 这是 U-Boot 最常用的优化级别
KBUILD_CFLAGS	+= -Os
endif

ifdef CONFIG_CC_OPTIMIZE_FOR_SPEED
# -O2: 优化执行速度(某些性能敏感场景)
KBUILD_CFLAGS	+= -O2
endif

ifdef CONFIG_CC_OPTIMIZE_FOR_DEBUG
# -Og: 优化调试体验
# 在不严重影响调试的前提下进行最小优化
# 比 -O0 生成更好的代码,同时保持大部分变量可观察
KBUILD_CFLAGS	+= -Og
# GCC 在 -Og 下可能产生误报的 "maybe-uninitialized" 警告
# 参见 GCC Bug #78394
KBUILD_CFLAGS	+= -Wno-maybe-uninitialized
endif

# LTO (Link-Time Optimization) 链接时优化标志
# 允许编译器在链接阶段跨编译单元进行优化
# 此处初始化为空,由架构 Makefile 按需设置
LTO_CFLAGS :=
LTO_FINAL_LDFLAGS :=
export LTO_CFLAGS LTO_FINAL_LDFLAGS

头文件包含路径

UBOOTINCLUDE: 统一的头文件搜索路径

  • -Iinclude: U-Boot 通用头文件目录

  • -I$(srctree)/include: 外部构建时,源码目录的 include

  • -I$(srctree)/arch/$(ARCH)/include: 架构特定头文件

  • -include $(srctree)/include/linux/kconfig.h: 强制所有编译单元包含此文件(-include 而非 -I)

kconfig.h 定义了 IS_ENABLED(),IS_BUILTIN() 等宏,这些宏将 CONFIG_xxx 转换为编译时布尔判断,使得 C 代码可以写: if (IS_ENABLED(CONFIG_FOO)) { … },编译器会在优化阶段消除不可达代码,替代 #ifdef。

UBOOTINCLUDE    := \
	-Iinclude \
	$(if $(KBUILD_SRC), -I$(srctree)/include) \
	-I$(srctree)/arch/$(ARCH)/include	\
	-include $(srctree)/include/linux/kconfig.h

# -nostdinc: 不搜索标准系统头文件目录
#   U-Boot 是独立环境,不能使用宿主机的 stdio.h 等
# -isystem: 但仍需要编译器内建头文件(如 stdarg.h, stddef.h, stdint.h),这些是编译器提供的,与操作系统无关
#   $(CC) -print-file-name=include 输出编译器内建头文件路径
#   例如: /usr/lib/gcc/arm-linux-gnueabihf/11/include
NOSTDINC_FLAGS += -nostdinc -isystem $(shell $(CC) -print-file-name=include)

# 组合完整的预处理和编译标志
cpp_flags := $(KBUILD_CPPFLAGS) $(PLATFORM_CPPFLAGS) $(UBOOTINCLUDE) \
							$(NOSTDINC_FLAGS)
c_flags := $(KBUILD_CFLAGS) $(cpp_flags)

编译目标

libs-y 变量收集所有需要编译的子目录,每个目录后面的 ‘/‘ 是 Kbuild 约定:表示需要递归进入该目录。在实际 U-Boot 中,这个列表会很长,包含:arch/$(ARCH)/lib/, cpu/, board/, common/, lib/, drivers/ 等

libs-y += lib/

# sort 去重并按字母排序
# 排序虽然改变了链接顺序,但 U-Boot 的启动入口由链接脚本的 ENTRY() 指令决定,而非链接顺序
libs-y := $(sort $(libs-y))

# u-boot-dirs: 去掉尾部 '/' 得到纯目录名列表
# filter %/: 过滤出以 '/' 结尾的项(即需要递归的目录)
# patsubst %/,%: 去掉尾部的 '/'
# 例如: "common/ lib/ drivers/" → "common lib drivers"
u-boot-dirs	:= $(patsubst %/,%,$(filter %/, $(libs-y)))

# u-boot-alldirs: 包含所有目录(含 libs- 即被禁用的目录)
# 用于 clean 等操作——即使被禁用的目录也需要清理
u-boot-alldirs	:= $(sort $(u-boot-dirs) $(patsubst %/,%,$(filter %/, $(libs-))))

# 将目录名转换为 built-in.o 路径
# 例如: "common/ lib/" → "common/built-in.o lib/built-in.o"
# built-in.o 是每个目录的编译产物集合(由 Makefile.build 生成)
# 它是该目录下所有 obj-y 目标的归档文件
libs-y		:= $(patsubst %/, %/built-in.o, $(libs-y))

# u-boot-init: 启动代码(必须最先链接)
# head-y 由架构 Makefile 定义,通常指向:arch/arm/cpu/armv7/start.o (ARM 示例)
# 这是 U-Boot 的入口点,包含复位向量和初始化代码
u-boot-init := $(head-y)

# u-boot-main: 其余所有编译产物
u-boot-main := $(libs-y)

# 链接脚本 (.lds) 通常需要预处理以支持条件编译
# LDPPFLAGS 是传给 CPP 的标志:
#   -include u-boot.lds.h: 强制包含链接脚本头文件
#   -DCPUDIR: 传递 CPU 目录路径
#   -DLD_MAJOR/-DLD_MINOR: 链接器版本号,用于兼容性处理
LDPPFLAGS += \
	-include $(srctree)/include/u-boot/u-boot.lds.h \
	-DCPUDIR=$(CPUDIR) \
	$(shell $(LD) --version | \
	  sed -ne 's/GNU ld version \([0-9][0-9]*\)\.\([0-9][0-9]*\).*/-DLD_MAJOR=\1 -DLD_MINOR=\2/p')

# INPUTS-y: 最终要生成的二进制文件列表
INPUTS-y += u-boot.bin

# 链接标志
# LDFLAGS_FINAL: 由架构 Makefile 定义的最终链接选项
# -Ttext: 指定 .text 段的加载地址
# CONFIG_TEXT_BASE 在 Kconfig 中配置,例如 0x87800000
LDFLAGS_u-boot += $(LDFLAGS_FINAL)
LDFLAGS_u-boot += -Ttext $(CONFIG_TEXT_BASE)

# objcopy 标志:将 ELF 转换为纯二进制(raw binary)
# -O binary: 输出格式为原始二进制
# 结果文件可以直接烧写到 Flash 或通过 JTAG 加载
OBJCOPYFLAGS_u-boot.bin := -O binary

# 命令定义
# Kbuild 的命令定义约定:
#   quiet_cmd_xxx: V=0 时显示的简短信息
#   cmd_xxx:       实际执行的命令
# $(call if_changed,xxx) 会:
#   1) 检查命令是否与上次相同(通过 .cmd 文件)
#   2) 检查依赖是否有变化
#   3) 如果需要重新执行,根据 $(quiet) 选择输出格式
quiet_cmd_objcopy = OBJCOPY $@
cmd_objcopy = $(OBJCOPY) $(OBJCOPYFLAGS) \
	$(OBJCOPYFLAGS_$(@F)) $< $@

quiet_cmd_objcopy_uboot = OBJCOPY $@
cmd_objcopy_uboot = $(cmd_objcopy)

# cfg 目标:生成 u-boot.cfg 配置摘要文件
cfg: u-boot.cfg

# binman 集成
# binman 是 U-Boot 的二进制打包工具
# 它负责将 u-boot.bin、SPL、ATF、设备树等组合成最终的烧写镜像
# .binman_stamp 是时间戳文件,确保每次构建都运行 binman
.binman_stamp: $(INPUTS-y) FORCE
ifeq ($(CONFIG_BINMAN),y)
	$(call if_changed,binman)
endif
	@touch $@

# all 是默认构建目标,依赖 .binman_stamp
all: .binman_stamp

# u-boot.bin 生成规则
# u-boot (ELF) → u-boot.bin (raw binary)
# FORCE 确保每次 make 都检查是否需要重新生成
u-boot.bin: u-boot FORCE
	$(call if_changed,objcopy_uboot)

# u-boot (ELF) 链接规则
# 这是整个构建系统最核心的规则——将所有目标文件链接为 ELF
#
# cmd_u-boot__ 的执行步骤:
#   1) touch $(u-boot-main): 确保 built-in.o 文件存在(防止空目录报错)
#   2) $(LD) 链接:
#      $(KBUILD_LDFLAGS): 全局链接选项
#      $(LDFLAGS_u-boot): u-boot 特定选项(-Ttext 等)
#      -o $@:             输出文件 u-boot
#      -T u-boot.lds:     使用链接脚本
#      $(u-boot-init):    启动代码(head-y,如 start.o)
#      $(u-boot-main):    主体代码(各目录的 built-in.o)
#      -Map u-boot.map:   生成内存映射文件,用于调试
quiet_cmd_u-boot__ ?= LD      $@
      cmd_u-boot__ ?=								\
		touch $(u-boot-main) ;						\
		$(LD) $(KBUILD_LDFLAGS) $(LDFLAGS_u-boot) -o $@			\
		-T u-boot.lds $(u-boot-init)					\
			$(u-boot-main)						\
		-Map u-boot.map

# u-boot 目标规则
# 依赖:
#   $(u-boot-init): 启动代码 .o 文件
#   $(u-boot-main): 各目录的 built-in.o
#   u-boot.lds:     链接脚本(已预处理)
#   FORCE:          强制检查
#
# +$(call if_changed,...): 前缀 '+' 的作用:
#   即使 make 以 -n (dry-run) 或 -t (touch) 模式运行,
#   也强制执行此命令。这是因为链接步骤对于验证构建至关重要
u-boot:	$(u-boot-init) $(u-boot-main) u-boot.lds FORCE
	+$(call if_changed,u-boot__)

递归构建机制

Makefile.build 是 Kbuild 的核心递归脚本,它会:

  1. 进入目标目录
  2. 读取该目录的 Makefile(或 Kbuild 文件)
  3. 获取 obj-y += xxx.o 列表
  4. 编译所有 .c/.S → .o
  5. 将 obj-y 归档为 built-in.o
  6. 如果 obj-y 中有目录(如 obj-y += subdir/),递归处理
# built-in.o 文件本身不需要显式规则
# 它们在递归进入子目录时由 Makefile.build 自动生成
# 空规则防止隐式规则干扰
# sort 去重,避免同一目标被执行多次
$(sort $(u-boot-init) $(u-boot-main)): $(u-boot-dirs) ;

# 核心递归规则:
# 对 u-boot-dirs 中的每个目录,执行:
#   make -f scripts/Makefile.build obj=<dir>
#
# 依赖 prepare 和 scripts:确保头文件、配置、工具都已就绪
#
# Locale 设置注释说明:
#   这里不再修改 locale,因为全局已设置为 C locale
#   错误信息仍使用用户的原始语言(未 export LC_MESSAGES)
PHONY += $(u-boot-dirs)
$(u-boot-dirs): prepare scripts
	$(Q)$(MAKE) $(build)=$@

构建准备阶段(prepare 链)

# 版本信息生成
# setlocalversion 脚本来自 Linux 内核
# 它生成类似 "-00001-gabcdef1234-dirty" 的版本后缀
# KERNELVERSION 变量是脚本所需的环境变量(沿用 Linux 的命名)
define filechk_uboot.release
	KERNELVERSION=$(UBOOTVERSION) $(CONFIG_SHELL) $(srctree)/scripts/setlocalversion $(srctree)
endef

# filechk 宏(定义在 Kbuild.include 中)的工作方式:
#   1) 执行 filechk_xxx 生成新内容到临时文件
#   2) 与现有文件比较
#   3) 仅在内容改变时才替换文件
#   4) 这避免了不必要的重编译(文件时间戳不变,依赖不触发)
include/config/uboot.release: include/config/auto.conf FORCE
	$(call filechk,uboot.release)

# 构建准备阶段
# prepare 是一个多阶段的准备链,从 prepare3 到 prepare0 逐级执行:
#   prepare3: 外部构建环境检查
#   prepare2: 生成输出目录的 Makefile
#   prepare1: 生成版本头文件 + 检查链接脚本
#   archprepare: 架构特定准备 + 编译工具
#   prepare0: 处理根目录的 Kbuild 文件
#   prepare: 最终就绪(其他规则依赖此目标)

PHONY += prepare archprepare prepare0 prepare1 prepare2 prepare3

# prepare3: 外部构建时的完整性检查
# 确保源码目录是"干净"的——不包含之前构建的残留物
# 如果源码目录中有 .config 或 include/config/,说明有人在源码目录中执行过构建,这可能导致冲突
prepare3: include/config/uboot.release
ifneq ($(KBUILD_SRC),)
	@$(kecho) '  Using $(srctree) as source for U-Boot'
	$(Q)if [ -f $(srctree)/.config -o -d $(srctree)/include/config ]; then \
		echo >&2 "  $(srctree) is not clean, please run 'make mrproper'"; \
		echo >&2 "  in the '$(srctree)' directory.";\
		/bin/false; \
	fi;
endif

# prepare2: 确保输出目录有可用的 Makefile
prepare2: prepare3 outputmakefile cfg

# prepare1: 生成核心头文件 + 配置验证
# 依赖:
#   version_h:  include/generated/version_autogenerated.h
#   timestamp_h: include/generated/timestamp_autogenerated.h
#   auto.conf:  确保配置已同步
# 检查链接脚本是否存在(LDSCRIPT 为空或指向不存在的文件时报错)
prepare1: prepare2 $(version_h) $(timestamp_h) \
				include/config/auto.conf
ifeq ($(wildcard $(LDSCRIPT)),)
	@echo >&2 "  Could not find linker script."
	@/bin/false
endif

# archprepare: 编译架构相关的准备工具
# tools 目标会编译如 mkimage、imxdownload 等镜像打包工具
archprepare: prepare1 scripts_basic tools

# prepare0: 处理根目录的 Kbuild 文件
# $(Q)$(MAKE) $(build)=. 会读取根目录的 Kbuild 文件
# 用于生成根目录级的头文件或配置
prepare0: archprepare FORCE
	$(Q)$(MAKE) $(build)=.

# 最终的 prepare 目标——所有子目录编译前的"总集结点"
prepare: prepare0

# filechk_version.h: 生成版本信息头文件
# 输出示例:
#   #define PLAIN_VERSION "2025.04-rc3-00001-gabcdef1234"
#   #define U_BOOT_VERSION "U-Boot " PLAIN_VERSION
#   #define U_BOOT_VERSION_NUM 2025
#   #define U_BOOT_VERSION_NUM_PATCH 4       ← 注意去除前导零!
#   #define HOST_ARCH 2                       ← HOST_ARCH_X86_64 的数值
#   #define CC_VERSION_STRING "arm-linux-gnueabihf-gcc (Ubuntu 11.3.0) 11.3.0"
#   #define LD_VERSION_STRING "GNU ld (GNU Binutils) 2.38"
#
# sed "s/^0*//": 去除 PATCHLEVEL 的前导零
#   "04" → "4"
#   这是必要的,因为 C 预处理器会将 "04" 视为八进制数
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

# filechk_timestamp.h: 生成构建时间戳头文件
# 支持 SOURCE_DATE_EPOCH 环境变量实现可复现构建(Reproducible Build):
#   如果设置了 SOURCE_DATE_EPOCH(Unix 时间戳),使用该固定时间
#   否则使用当前时间
#
# 可复现构建的意义:
#   相同的源码 + 相同的工具链 → 二进制级别相同的输出
#   对安全审计和供应链验证至关重要
#
# GNU date vs BSD date 兼容性:
#   GNU date 支持 -d "@timestamp" 格式
#   BSD date (macOS) 使用不同的语法
#   脚本尝试 gdate、date.gnu、date 三种命令名
#
# 输出示例:
#   #define U_BOOT_DATE "Jan 15 2025"
#   #define U_BOOT_TIME "10:30:45"
#   #define U_BOOT_TZ "+0800"
#   #define U_BOOT_EPOCH 1736930445
define filechk_timestamp.h
	(if test -n "$${SOURCE_DATE_EPOCH}"; then \
		SOURCE_DATE="@$${SOURCE_DATE_EPOCH}"; \
		DATE=""; \
		for date in gdate date.gnu date; do \
			$${date} -u -d "$${SOURCE_DATE}" >/dev/null 2>&1 && DATE="$${date}"; \
		done; \
		if test -n "$${DATE}"; then \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_DATE "%b %d %C%y"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TIME "%T"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_TZ "%z"'; \
			LC_ALL=C $${DATE} -u -d "$${SOURCE_DATE}" +'#define U_BOOT_EPOCH %s'; \
		else \
			return 42; \
		fi; \
	else \
		LC_ALL=C date +'#define U_BOOT_DATE "%b %d %C%y"'; \
		LC_ALL=C date +'#define U_BOOT_TIME "%T"'; \
		LC_ALL=C date +'#define U_BOOT_TZ "%z"'; \
		LC_ALL=C date +'#define U_BOOT_EPOCH %s'; \
	fi)
endef

# version_h 和 timestamp_h 的生成规则
# filechk 宏确保只在内容真正改变时才更新文件
# 这避免了因时间戳变化导致的不必要重编译链
$(version_h): include/config/uboot.release FORCE
	$(call filechk,version.h)

# timestamp_h 依赖顶层 Makefile(因为 VERSION 等变量在此定义)
$(timestamp_h): $(srctree)/Makefile FORCE
	$(call filechk,timestamp.h)

# 工具编译
# tools 目标:编译构建过程中使用的宿主机工具
# tools_imxdownload: NXP i.MX 系列处理器的镜像下载工具
# 在实际 U-Boot 中还包括 mkimage、mkenvimage 等
PHONY += tools
tools: tools_imxdownload

# imxdownload 工具编译
PHONY += tools_imxdownload
tools_imxdownload: scripts_basic
	$(Q)$(MAKE) $(build)=tools

# DTC(设备树编译器)构建
# 场景 1: 使用内建 DTC,编译 scripts/dtc/ 下的源码
#
# 场景 2: 使用外部 DTC,检查版本是否满足最低要求,
#        如果启用了 CONFIG_PYLIBFDT,检查 Python 的 libfdt 绑定
#
# pylibfdt 是设备树的 Python 绑定库,U-Boot 的 binman 工具使用它来操作设备树
PHONY += scripts_dtc
scripts_dtc: scripts_basic
	$(Q)if test "$(DTC)" = "$(DTC_INTREE)"; then \
    	$(MAKE) $(build)=scripts/dtc; \
	else \
    	if ! $(DTC) -v > /dev/null; then \
		echo '*** Failed to check dtc version: $(DTC)'; \
    	false; \
    else \
    	if test "$(call dtc-version)" -lt $(DTC_MIN_VERSION); then \
			echo '*** Your dtc is too old, please upgrade to dtc $(DTC_MIN_VERSION) or newer'; \
			false; \
		else \
			if [ -n "$(CONFIG_PYLIBFDT)" ]; 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

# 链接脚本预处理
# U-Boot 的链接脚本(.lds)需要 C 预处理器处理
# 原因:链接脚本中使用了 #include、#ifdef、CONFIG_xxx 等
# 标志说明:
#   -Wp,-MD,$(depfile): 生成依赖文件(用于增量编译)
#   $(cpp_flags):        C 预处理标志(包含路径、宏定义等)
#   $(LDPPFLAGS):        链接脚本专用标志(CPUDIR、LD 版本等)
#   -D__ASSEMBLY__:      告知头文件这不是 C 编译
#   -x assembler-with-cpp: 告知 GCC 输入是需要预处理的汇编/脚本
#   -std=c99:            使用 C99 预处理器标准
#   -P:                  不生成行号标记(#line),链接器不识别它们
quiet_cmd_cpp_lds = LDS     $@
cmd_cpp_lds = $(CPP) -Wp,-MD,$(depfile) $(cpp_flags) $(LDPPFLAGS) \
		-D__ASSEMBLY__ -x assembler-with-cpp -std=c99 -P -o $@ $<

# u-boot.lds 生成规则
# if_changed_dep: 同 if_changed,但额外处理依赖文件(.d to .cmd 转换)
u-boot.lds: $(LDSCRIPT) prepare FORCE
	$(call if_changed_dep,cpp_lds)

清理目标

清理系统实现三级渐进式清理策略,每一级都在前一级的基础上删除更多文件:

  • make clean

目的:删除大部分编译产物,保留足以支持外部模块编译的最小文件集

保留:配置文件 (.config)、Kconfig 生成的头文件、部分工具

删除:目标文件 (.o)、可执行文件 (u-boot*)、依赖文件 (.d/.cmd)

使用场景:快速重新编译,无需重新配置

  • make mrproper

目的:删除所有生成的文件,包括配置,恢复到刚解压源码的状态

删除:clean 删除的所有内容 + .config + auto.conf + tags 文件

使用场景:切换板卡配置、提交代码到版本控制前

  • make distclean (distribution clean)

目的:删除 mrproper 的内容 + 开发工具残留文件

删除:mrproper 删除的内容 + 编辑器备份文件 + patch 残留

使用场景:打包发布前的最终清理、清理 Git 未跟踪的垃圾文件

# Directories & files removed with 'make clean'
CLEAN_DIRS  += 

CLEAN_FILES += include/autoconf.mk* include/config.h u-boot* System.map \
	       defconfig

# Directories & files removed with 'make mrproper'
MRPROPER_DIRS  += include/config include/generated include/asm

# 需要精确删除的文件(包含配置文件)
MRPROPER_FILES += .config .config.old include/autoconf.mk* include/config.h

# GNU Make 的"目标特定变量"特性:
#   clean: rm-dirs := $(CLEAN_DIRS)
# 等价于:
#   在执行 clean 目标时,临时设置 rm-dirs = $(CLEAN_DIRS)
#   执行完毕后,rm-dirs 恢复原值(如果有)rm-dirs 只在 clean 目标及其依赖中有效
clean: rm-dirs  := $(CLEAN_DIRS)
clean: rm-files := $(CLEAN_FILES)

# 收集需要递归清理的子目录
# u-boot-alldirs包含所有 libs-y 和 libs- 的目录
# 逻辑:
#   1) 遍历 u-boot-alldirs(所有源码目录,包括被禁用的)
#   2) 检查每个目录是否存在 Makefile($(wildcard $(srctree)/$f/Makefile))
#   3) 只有存在 Makefile 的目录才需要递归清理
clean-dirs	:= $(foreach f,$(u-boot-alldirs),$(if $(wildcard $(srctree)/$f/Makefile),$f))

# 为每个目录添加 _clean_ 前缀,构造伪目标名称
clean-dirs      := $(addprefix _clean_, $(clean-dirs))

PHONY += $(clean-dirs) clean archclean
# 递归清理子目录的规则(模式规则)
# -----------------------------------------------------------------------
# $(clean-dirs) 包含 _clean_common, _clean_drivers 等
# 对每个 _clean_xxx 目标:
#   1) $(patsubst _clean_%,%,$@) 提取目录名(_clean_common → common)
#   2) $(clean) 在 Kbuild.include 中定义为:
#        clean := -f $(srctree)/scripts/Makefile.clean obj
#   3) 展开后的命令:
#        $(Q)make -f scripts/Makefile.clean obj=common
#   4) Makefile.clean 会:
#        - 读取 common/Makefile
#        - 删除 obj-y 中所有编译产物(.o, .cmd, .d 等)
#        - 删除 clean-files 指定的文件
#        - 递归进入 subdir-y 子目录
$(clean-dirs):
	$(Q)$(MAKE) $(clean)=$(patsubst _clean_%,%,$@)

clean: $(clean-dirs)
	$(call cmd,rmdirs)
	$(call cmd,rmfiles)
	@find . $(RCS_FIND_IGNORE) \
		\( -name '*.[oas]' -o -name '.*.cmd' \
		-o -name '.*.d' -o -name '.*.tmp' \
		-o -name '*.lex.c' -o -name '*.tab.[ch]' \
		-o -name 'generated_defconfig' \
		-o -name '*.efi' -o -name '*.gcno' -o -name '*.so' \) \
		-type f -print | xargs rm -f

# mrproper - Delete all generated files, including .config
#
mrproper: rm-dirs  := $(wildcard $(MRPROPER_DIRS))
mrproper: rm-files := $(wildcard $(MRPROPER_FILES))
mrproper-dirs      := $(addprefix _mrproper_,scripts)

PHONY += $(mrproper-dirs) mrproper archmrproper
$(mrproper-dirs):
	$(Q)$(MAKE) $(clean)=$(patsubst _mrproper_%,%,$@)

mrproper: clean $(mrproper-dirs)
	$(call cmd,rmdirs)
	$(call cmd,rmfiles)
	@rm -f arch/*/include/asm/arch

# distclean
#
PHONY += distclean

distclean: mrproper
	@find $(srctree) $(RCS_FIND_IGNORE) \
		\( -name '*.orig' -o -name '*.rej' -o -name '*~' \
		-o -name '*.bak' -o -name '#*#' -o -name '.*.orig' \
		-o -name '.*.rej' -o -name '*%' -o -name 'core' \
		-o -name '*.pyc' \) \
		-type f -print | xargs rm -f

quiet_cmd_rmdirs = $(if $(wildcard $(rm-dirs)),CLEAN   $(wildcard $(rm-dirs)))
      cmd_rmdirs = rm -rf $(rm-dirs)

quiet_cmd_rmfiles = $(if $(wildcard $(rm-files)),CLEAN   $(wildcard $(rm-files)))
      cmd_rmfiles = rm -f $(rm-files)

# .cmd 文件是 Kbuild 增量编译系统的核心
# 每次编译命令执行后,if_changed 宏会生成对应的 .cmd 文件
# 下次 make 时:
#   1) include .main.o.cmd,加载上次的命令和依赖
#   2) if_changed 比较当前命令与 cmd_main.o
#   3) 如果命令改变(如 CFLAGS 变了),重新编译
#   4) 如果依赖文件改变,重新编译
#   5) 都没变,跳过编译

# 找到所有 .cmd 文件
cmd_files := $(wildcard .*.cmd)

ifneq ($(cmd_files),)
  # 空规则防止 make 尝试重建 .cmd 文件
  $(cmd_files): ;
  # 包含所有 .cmd 文件,加载历史命令和依赖信息
  include $(cmd_files)
endif

endif #ifeq ($(config-targets),1)
endif #ifeq ($(mixed-targets),1)
endif	# skip-makefile

# FORCE 目标:Kbuild 的"万能依赖"
# 它永远被视为"已更新"(因为没有规则来创建它,也不是文件)
# 任何依赖 FORCE 的目标都会被重新评估(但不一定重新执行)
#
# 与 .PHONY 的区别:
#   .PHONY 目标总是执行其命令
#   依赖 FORCE 的目标只在命令或依赖改变时执行(通过 if_changed 检查)
#
# 这是一个关键的优化:
#   例如 u-boot.bin 依赖 FORCE,但如果 u-boot 没有改变,if_changed 会跳过 objcopy,避免不必要的工作
PHONY += FORCE
FORCE:

# 统一声明所有伪目标
# 将 PHONY 变量中收集的所有目标名声明为 .PHONY
# 这比在每个规则处单独声明更清晰、更不易遗漏
.PHONY: $(PHONY)

4. Kbuild递归构建Makefile.build

Makefile.build — Kbuild 递归构建系统的核心引擎。

调用入口(来自顶层 Makefile 或上级 Makefile.build):

$(Q)$(MAKE) $(build)=<dir>

展开为:

@make -f scripts/Makefile.build obj=<dir>

其中 obj=

是通过命令行传入的变量,指定当前处理的目录

例如:obj=lib, obj=drivers/gpio, obj=arch/arm/cpu

# 检查是否有 vpl/ 前缀
# patsubst 尝试从 obj 中去掉 vpl/ 前缀
# 如果 obj=vpl/lib,则 src=lib(后续条件不相等,则有vpl前缀)
# 如果 obj=lib,则 src=lib(无变化,说明没有 vpl/ 前缀,继续比较tpl,spl)
prefix := vpl
src := $(patsubst $(prefix)/%,%,$(obj))

ifeq ($(obj),$(src))
# obj 与 src 相同,说明没有 vpl/ 前缀,继续尝试 tpl/
prefix := tpl
src := $(patsubst $(prefix)/%,%,$(obj))

ifeq ($(obj),$(src))
# 也没有 tpl/ 前缀,继续尝试 spl/
prefix := spl
src := $(patsubst $(prefix)/%,%,$(obj))

ifeq ($(obj),$(src))
# 三个前缀都不匹配,这是主 U-Boot 构建
# prefix 设为 ".",表示当前目录(无前缀)
prefix := .
endif
endif
endif

# __build 是 Makefile.build 的默认目标
# 当执行 make -f Makefile.build obj=xxx 时,如果不指定目标,
# 就会执行 __build
#
# 此处先声明为空,后面会用完整依赖覆盖
# PHONY 声明确保每次都重新评估
PHONY := __build
__build:

# --- 编译目标变量 ---
obj-y :=     # 需要编译并链接到最终二进制的目标列表
             # 子目录 Makefile 中填充:
             #   obj-y += foo.o        → 编译 foo.c 为 foo.o
             #   obj-$(CONFIG_BAR) += bar.o  → 条件编译
             #   obj-y += subdir/      → 递归进入 subdir/

lib-y :=     # 编译为库文件的目标(与 obj-y 类似但语义不同)
             # 通常用于 lib/ 目录下的通用库函数

targets :=   # 所有编译目标的完整列表
             # 用于 .cmd 文件的查找和加载
             # 包括 obj-y + extra-y + always + lib-y

subdir-y :=  # 需要递归进入的子目录列表
             # 由 Makefile.lib 从 obj-y 中提取
             # obj-y += subdir/ → subdir-y += subdir

# --- 编译标志变量 ---
EXTRA_AFLAGS   :=   # 额外的汇编器标志(旧式接口,建议用 asflags-y)
EXTRA_CFLAGS   :=   # 额外的 C 编译器标志(旧式接口,建议用 ccflags-y)
EXTRA_LDFLAGS  :=   # 额外的链接器标志

asflags-y  :=       # 当前目录的汇编标志(新式接口)
ccflags-y  :=       # 当前目录的 C 编译标志(新式接口)
ldflags-y  :=       # 当前目录的链接标志

# --- 子目录继承标志 ---
subdir-asflags-y :=  # 传递给子目录的汇编标志
subdir-ccflags-y :=  # 传递给子目录的 C 编译标志
                     # 这些标志会向下传递到所有子目录
                     # 用于在父目录统一设置编译选项

# 加载 Kconfig 生成的配置,使得子目录 Makefile 中的,obj-$(CONFIG_FOO) 条件编译生效
# include/config/auto.conf 包含所有 CONFIG_xxx=y 的定义
# -include: 文件不存在时不报错(首次构建时尚未生成)
-include include/config/auto.conf
-include $(prefix)/include/autoconf.mk

# Kbuild.include 定义了核心宏和函数
include scripts/Kbuild.include

# $(src) 可能是相对路径(如 lib)或绝对路径(如 /opt/u-boot/lib)
# 如果 src 以 / 开头,绝对路径,直接使用
# 否则 → 相对于 $(srctree) 的路径,需要拼接
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))
# 选择 Kbuild 文件或 Makefile,查找优先级:
#   1. $(kbuild-dir)/Kbuild    ← 优先(Kbuild 专用文件)
#   2. $(kbuild-dir)/Makefile  ← 降级(传统 Makefile)
# U-Boot 中,大部分目录仍使用 Makefile
kbuild-file := $(if $(wildcard $(kbuild-dir)/Kbuild),$(kbuild-dir)/Kbuild,$(kbuild-dir)/Makefile)
include $(kbuild-file)

PLATFORM_CPPFLAGS 是架构 Makefile(如 arch/arm/Makefile)定义的平台特定预处理器标志
asflags-y  += $(PLATFORM_CPPFLAGS)
ccflags-y  += $(PLATFORM_CPPFLAGS)
cppflags-y += $(PLATFORM_CPPFLAGS)

# Makefile.lib 是 Kbuild 的"中间层",负责:
# 1. 从 obj-y 中分离出子目录和文件
# 2. 为目标添加路径前缀
# 3. 计算最终编译标志
# 4. 处理条件编译
include scripts/Makefile.lib

# 宿主机程序编译支持。条件包含:只有定义了 hostprogs-y 才加载
# 避免不必要的规则加载和潜在冲突
ifneq ($(hostprogs-y),)
include scripts/Makefile.host
endif

# 确保编译产物的输出目录存在
# shell 命令在 Makefile 解析阶段执行(不是在规则执行阶段)
# 使用 _dummy 变量承接 shell 的返回值(实际不使用该值)
_dummy := $(shell [ -d $(obj) ] || mkdir -p $(obj))
# 当 Makefile 中使用 obj-y := dir/file.o 语法时
# 目标文件 dir/file.o 的目录 dir/ 可能不存在
# $(obj-dirs) 由 Makefile.lib 计算,包含所有需要创建的子目录
_dummy := $(foreach d,$(obj-dirs), $(shell [ -d $(d) ] || mkdir -p $(d)))

# 确保 obj 变量已正确传入
# 如果有人直接执行 make -f Makefile.build 而不传 obj=,会报警
# 这是一个防御性检查,帮助开发者发现错误用法
ifndef obj
$(warning kbuild: Makefile.build is included improperly)
endif

# built-in.o 是整个目录的编译产物集合:
#   lib/built-in.o 包含 lib/ 下所有 obj-y 的 .o 文件,它最终参与 u-boot 的链接
ifneq ($(strip $(obj-y) $(obj-)),)
builtin-target := $(obj)/built-in.o
endif

#   __build 本身不执行任何操作,所有工作由其依赖的规则完成
__build: $(builtin-target) $(extra-y) $(subdir-ym) $(always)
	@:

# quiet_modtag:模块标签(U-Boot 不支持模块,此处为空)
#   在 Linux 内核中:quiet_modtag = [M](标识模块编译)
#   在 U-Boot 中:空字符串 + 两个空格的间距对齐
quiet_modtag := $(empty)   $(empty)

#   quiet_cmd_cc_s_c:V=0 时的简短输出
#     示例输出:"  CC      lib/string.s"
#   cmd_cc_s_c:实际命令
#     $(CC):交叉编译器(如 arm-linux-gnueabihf-gcc)
#     $(c_flags):完整的 C 编译标志(由 Makefile.lib 计算)
#     $(DISABLE_LTO):禁用 LTO(生成汇编时不应做链接时优化)
#     -fverbose-asm:在汇编输出中添加 C 源码注释
#     -S:只编译到汇编阶段,不生成 .o
quiet_cmd_cc_s_c = CC $(quiet_modtag)  $@
cmd_cc_s_c       = $(CC) $(c_flags) $(DISABLE_LTO) -fverbose-asm -S -o $@ $<


#   $(obj)/%.s:目标文件(如 lib/string.s)
#   $(src)/%.c:源文件(如 lib/string.c)
#   FORCE:强制检查是否需要重新生成
#   if_changed_dep:增量编译 + 依赖文件处理(通过 fixdep)
$(obj)/%.s: $(src)/%.c FORCE
	$(call if_changed_dep,cc_s_c)

# .c → .i(生成预处理后的 C 代码,用于调试宏展开)
quiet_cmd_cc_i_c = CPP $(quiet_modtag) $@
cmd_cc_i_c       = $(CPP) $(c_flags)   -o $@ $<

$(obj)/%.i: $(src)/%.c FORCE
	$(call if_changed_dep,cc_i_c)

#   quiet_cmd_cc_o_c:V=0 时输出 "  CC      lib/string.o"
#   cmd_cc_o_c:实际编译命令
#     $(CC):交叉编译器
#     $(c_flags):完整编译标志(来自 Makefile.lib)
#       = KBUILD_CFLAGS      (全局标志:-Wall -Os 等)
#       + ccflags-y           (目录级标志:子目录 Makefile 定义)
#       + CFLAGS_$(@F)        (文件级标志:如 CFLAGS_string.o = -O2)
#       + NOSTDINC_FLAGS      (-nostdinc -isystem ...)
#       + UBOOTINCLUDE        (-Iinclude -Iarch/arm/include ...)
#     -c:只编译不链接
#     -o $@:输出文件
#     $<:输入文件(第一个依赖,即 .c 文件)
quiet_cmd_cc_o_c = CC $(quiet_modtag)  $@
cmd_cc_o_c = $(CC) $(c_flags) -c -o $@ $<

define rule_cc_o_c
	$(call echo-cmd,cc_o_c) $(cmd_cc_o_c);				  \
	scripts/basic/fixdep $(depfile) $@ '$(call make-cmd,cc_o_c)' >    \
	                                              $(dot-target).tmp;  \
	rm -f $(depfile);						  \
	mv -f $(dot-target).tmp $(dot-target).cmd
endef

# .c → .o 的模式规则
# 每个 .c 文件都通过此规则编译为 .o
# $(call if_changed_rule,cc_o_c):
#      与 if_changed 类似,但执行 rule_cc_o_c 而非 cmd_cc_o_c
#      检查命令和依赖是否改变,仅在需要时重新编译
$(obj)/%.o: $(src)/%.c FORCE
	$(call if_changed_rule,cc_o_c)

# --- .S → .s(汇编文件预处理,用于调试)---
quiet_cmd_as_s_S = CPP $(quiet_modtag) $@
cmd_as_s_S       = $(CPP) $(a_flags)   -o $@ $<

$(obj)/%.s: $(src)/%.S FORCE
	$(call if_changed_dep,as_s_S)

# --- .S → .o(汇编编译,核心规则)---
# .S 文件包含 C 预处理指令(#include, #ifdef 等)
# $(CC)(GCC)会先调用预处理器展开这些指令,再调用汇编器
# 直接使用 $(AS) 无法处理预处理指令
quiet_cmd_as_o_S = AS $(quiet_modtag)  $@
cmd_as_o_S       = $(CC) $(a_flags) -c -o $@ $<

$(obj)/%.o: $(src)/%.S FORCE
	$(call if_changed_dep,as_o_S)

# targets 变量记录本目录中所有可能生成的目标文件
# 用途:
#   1. 查找对应的 .cmd 文件(增量编译支持)
#   2. 确保 .cmd 文件覆盖所有编译产物
#   3. V=2 调试输出中检查目标是否在列表中
#
# real-objs-y:真实的 .o 文件(排除子目录项)
# lib-y:库文件
# extra-y:额外编译目标
# MAKECMDGOALS:用户在命令行指定的目标
# always, always-y:无条件构建的目标
targets += $(real-objs-y) $(lib-y)
targets += $(extra-y) $(MAKECMDGOALS) $(always)
targets += $(lib-y) $(always-y)

# 链接脚本中使用 C 预处理指令来实现条件编译
quiet_cmd_cpp_lds_S = LDS     $@
      cmd_cpp_lds_S = $(CPP) $(cpp_flags) -P -C -U$(ARCH) \
	                     -D__ASSEMBLY__ -DLINKER_SCRIPT -o $@ $<

$(obj)/%.lds: $(src)/%.lds.S FORCE
	$(call if_changed_dep,cpp_lds_S)

# sort 去重,避免同一子目录被处理多次
# 空规则 ";" 表示目标由依赖自动满足,无需额外命令
$(sort $(subdir-obj-y)): $(subdir-ym) ;

# built-in.o 归档规则
ifdef builtin-target
quiet_cmd_link_o_target = AR      $@

cmd_link_o_target = $(if $(strip $(obj-y)),\
		      rm -f $@; $(AR) cDPrsT $@ $(filter $(obj-y), $^), \
		      rm -f $@; $(AR) cDPrsT$(KBUILD_ARFLAGS) $@)

$(builtin-target): $(obj-y) FORCE
	$(call if_changed,link_o_target)

# 将 built-in.o 加入 targets 列表,确保其 .cmd 文件被加载
targets += $(builtin-target)

endif # builtin-target

# 中间目标处理
# 某些目标文件是通过多步生成的:
#   .l 文件(Lex 源码)→ .lex.c(C 代码)→ .lex.o(目标文件)
#   .y 文件(Yacc 源码)→ .tab.c + .tab.h(C 代码 + 头文件)→ .tab.o
#
# intermediate_targets 函数:
#   从 targets 列表中找出最终目标(如 .lex.o),反向推导出中间目标(如 .lex.c)
define intermediate_targets
$(foreach sfx, $(2), \
	$(patsubst %$(strip $(1)),%$(sfx), \
		$(filter %$(strip $(1)), $(targets))))
endef

targets += $(call intermediate_targets, .lex.o, .lex.c) \
	   $(call intermediate_targets, .tab.o, .tab.c .tab.h)

# 对 subdir-ym 中的每个子目录,递归调用 Makefile.build
# subdir-ym 由 Makefile.lib 从 obj-y 中提取
# 顶层 Makefile → Makefile.build(lib) → Makefile.build(lib/gpio) → ...
# 每一层产生 built-in.o,上层将其归档到自己的 built-in.o 中
PHONY += $(subdir-ym)
$(subdir-ym):
	$(Q)$(MAKE) $(build)=$@

PHONY += FORCE
FORCE:

cmd_files := $(wildcard $(foreach f,$(sort $(targets)),$(dir $(f)).$(notdir $(f)).cmd))

ifneq ($(cmd_files),)
  include $(cmd_files)
endif

# 确保所有对象目录存在
obj-dirs := $(sort $(obj) $(patsubst %/,%, $(dir $(targets))))
$(shell mkdir -p $(obj-dirs))
.PHONY: $(PHONY)

5. Makefile.lib

Makefile.lib 负责将 Kbuild 文件中的对象列表(obj-ylib-y 等)转换为实际的构建目标和编译标志。

# scripts/Makefile.lib
# Kbuild 核心库文件:将 Kbuild 变量转换为构建目标和编译标志

# subdir-asflags-y 和 subdir-ccflags-y 用于向下传递到子目录
# 例如:在 drivers/Makefile 中设置 subdir-ccflags-y += -DDEBUG
#       则 drivers/ 下所有子目录都会使用这个标志
#
# export 确保这些变量在递归 make 时可见
# 使用 += 追加而非覆盖,实现累积效果
export KBUILD_SUBDIR_ASFLAGS := $(KBUILD_SUBDIR_ASFLAGS) $(subdir-asflags-y)
export KBUILD_SUBDIR_CCFLAGS := $(KBUILD_SUBDIR_CCFLAGS) $(subdir-ccflags-y)

# __subdir-y: 去掉尾部 '/',得到纯目录名
# 例如:obj-y = "foo.o drivers/ bar.o gpio/"
#       __subdir-y = "drivers gpio"
__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))

# 追加到显式声明的 subdir-y 中
# 这样 Kbuild 文件可以同时使用两种方式声明子目录:
#   subdir-y += subdir1        # 方式1:显式声明
#   obj-y += subdir2/          # 方式2:在 obj-y 中带 '/' 后缀
subdir-y	+= $(__subdir-y)

# 将 obj-y 中的子目录项替换为 built-in.o 路径
# 例如:obj-y = "foo.o drivers/ bar.o"
#       变为:obj-y = "foo.o drivers/built-in.o bar.o"
# 这样链接时直接包含子目录的归档文件
obj-y		:= $(patsubst %/, %/built-in.o, $(obj-y))

# 合并需要递归进入的子目录(去重并排序)
subdir-ym	:= $(sort $(subdir-y))

# Kbuild 支持"复合对象":一个 .o 文件由多个源文件组成
# suffix-search: 搜索带指定后缀的变量
# 参数:$1 = 目标名(如 foo.o)
#       $2 = 要替换的后缀(如 .o)
#       $3 = 要搜索的后缀列表(如 "-objs -y")
# 示例:$(call suffix-search, foo.o, .o, -objs -y)
#       展开为:$(foo-objs) $(foo-y)
suffix-search = $(strip $(foreach s, $3, $($(1:%$(strip $2)=%$s))))

# multi-search: 找出所有复合对象
# 返回 obj-y 中那些有 -objs 或 -y 定义的目标
# 例如:obj-y = "foo.o bar.o",foo-objs 有定义,bar-objs 无定义
#       返回:foo.o
multi-search = $(sort $(foreach m, $1, $(if $(call suffix-search, $m, $2, $3 -), $m)))

# real-search: 展开复合对象为其组成部分
# 如果目标有 -objs/-y 定义,返回其内容;否则返回目标本身
# 例如:obj-y = "foo.o bar.o",foo-objs = "a.o b.o"
#       返回:a.o b.o bar.o
real-search = $(foreach m, $1, $(if $(call suffix-search, $m, $2, $3 -), $(call suffix-search, $m, $2, $3), $m))

# subdir-obj-y: 从 obj-y 中提取子目录的 built-in.o
# 这些不是真正要编译的文件,而是要链接的归档文件
subdir-obj-y := $(filter %/built-in.o, $(obj-y))

# real-objs-y: 真正需要编译的对象文件列表
# 处理逻辑:
#   1) 从 obj-y 中排除子目录项(built-in.o)
#   2) 对每个目标,检查是否有 -objs 或 -y 定义
#   3) 如果有,展开为组成部分;否则保留原样
#   4) 追加 extra-y 中的目标
real-objs-y := $(foreach m, $(filter-out $(subdir-obj-y), $(obj-y)), \
        $(if $(strip $($(m:.o=-objs)) $($(m:.o=-y))),$($(m:.o=-objs)) \
        $($(m:.o=-y)),$(m))) $(extra-y)

# 将所有目标名前加上 $(obj)/ 目录前缀
# 这是实现外部构建(O=builddir)的关键:
# 源文件在 $(src)/,对象文件生成到 $(obj)/
extra-y		:= $(addprefix $(obj)/,$(extra-y))
always-y	:= $(addprefix $(obj)/,$(always-y))
always		:= $(addprefix $(obj)/,$(always))
targets		:= $(addprefix $(obj)/,$(targets))
obj-y		:= $(addprefix $(obj)/,$(obj-y))
lib-y		:= $(addprefix $(obj)/,$(lib-y))
subdir-obj-y	:= $(addprefix $(obj)/,$(subdir-obj-y))
real-objs-y	:= $(addprefix $(obj)/,$(real-objs-y))
subdir-ym	:= $(addprefix $(obj)/,$(subdir-ym))

# name-fix: 将文件名转换为合法的 C 标识符
# 替换 '-' 和 ',' 为 '_',并用引号包裹
# squote/quote 在 Kbuild.include 中定义:squote='  quote="
name-fix = $(squote)$(quote)$(subst $(comma),_,$(subst -,_,$1))$(quote)$(squote)

# basename_flags: 定义 KBUILD_BASENAME 宏
# 每个源文件编译时都定义这个宏,值为文件的基本名
# 用于调试输出、日志等场景
# 例如:编译 drivers/gpio/mxc_gpio.c 时-DKBUILD_BASENAME='"mxc_gpio"'
basename_flags = -DKBUILD_BASENAME=$(call name-fix,$(basetarget))

# C 编译标志组合
orig_c_flags   = $(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS) $(KBUILD_SUBDIR_CCFLAGS) \
                 $(ccflags-y) $(CFLAGS_$(basetarget).o)

# _c_flags: 过滤掉需要移除的标志
# CFLAGS_REMOVE_xxx.o 允许针对特定文件移除某些标志
# 例如:CFLAGS_REMOVE_broken.o += -Werror
_c_flags       = $(filter-out $(CFLAGS_REMOVE_$(basetarget).o), $(orig_c_flags))

# 汇编标志组合
orig_a_flags   = $(KBUILD_CPPFLAGS) $(KBUILD_AFLAGS) $(KBUILD_SUBDIR_ASFLAGS) \
                 $(asflags-y) $(AFLAGS_$(basetarget).o)
_a_flags       = $(filter-out $(AFLAGS_REMOVE_$(basetarget).o), $(orig_a_flags))

# 预处理标志
_cpp_flags     = $(KBUILD_CPPFLAGS) $(cppflags-y) $(CPPFLAGS_$(@F))

# 外部构建支持
# 当使用 O=builddir 外部构建时,需要调整 -I 路径
# 源文件中的 #include "foo.h" 需要找到 $(srctree)/dir/foo.h

ifeq ($(KBUILD_SRC),)
# 源码内构建:直接使用标志
__c_flags	= $(_c_flags)
__a_flags	= $(_a_flags)
__cpp_flags     = $(_cpp_flags)
else
# 外部构建:添加源码树路径
# -I$(obj): 定位生成的 .h 文件(在输出目录)
# $(call addtree,-I$(src)): 将相对路径转换为 $(srctree)/... 形式定位源码中的 .h 文件
# $(call flags,_c_flags): 处理 _c_flags 中的所有 -I 选项
__c_flags	= $(if $(obj),$(call addtree,-I$(src)) -I$(obj)) \
		  $(call flags,_c_flags)
__a_flags	= $(call flags,_a_flags)
__cpp_flags     = $(call flags,_cpp_flags)
endif

# 最终编译标志(传给编译器的完整命令行)
c_flags        = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE)     \
		 $(__c_flags) $(modkern_cflags)                           \
		 $(basename_flags)
a_flags        = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE)     \
		 $(__a_flags)
cpp_flags      = -Wp,-MD,$(depfile) $(NOSTDINC_FLAGS) $(UBOOTINCLUDE)     \
		 $(__cpp_flags)
ld_flags       = $(KBUILD_LDFLAGS) $(ldflags-y) $(LDFLAGS_$(@F))

# multi_depend: 为复合对象生成依赖规则
define multi_depend
$(foreach m, $(notdir $1), \
	$(eval $(obj)/$m: \
	$(addprefix $(obj)/, $(foreach s, $3, $($(m:%$(strip $2)=%$(s)))))))
endef

# LEX 词法分析器生成
# .l 文件 → .lex.c 文件
# 用于 Kconfig 解析器(scripts/kconfig/zconf.l)

quiet_cmd_flex = LEX     $@
      cmd_flex = $(LEX) -o$@ -L $<
# -o$@: 输出到目标文件
# -L:   不生成 #line 指令(简化输出)

$(obj)/%.lex.c: $(src)/%.l FORCE
	$(call if_changed,flex)

# YACC 语法分析器生成
# .y 文件 → .tab.c 和 .tab.h 文件
# 用于 Kconfig 解析器(scripts/kconfig/zconf.y)

# 生成 .tab.c(解析器实现)
quiet_cmd_bison = YACC    $@
      cmd_bison = $(YACC) -o$@ -t -l $<
# -o$@: 输出到目标文件
# -t:   启用调试(定义 YYDEBUG)
# -l:   不生成 #line 指令

$(obj)/%.tab.c: $(src)/%.y FORCE
	$(call if_changed,bison)

# 生成 .tab.h(解析器头文件,供词法分析器使用)
quiet_cmd_bison_h = YACC    $@
      cmd_bison_h = $(YACC) -o/dev/null --defines=$@ -t -l $<
# -o/dev/null: 丢弃 .c 输出(只要头文件)
# --defines=$@: 生成头文件到 $@

$(obj)/%.tab.h: $(src)/%.y FORCE
	$(call if_changed,bison_h)

# ASM 偏移量生成(用于汇编访问 C 结构体)
# 用途:让汇编代码可以使用 C 结构体的偏移量
# 例如:offsetof(struct pt_regs, ARM_pc) → #define ARM_pc 60
# sed 正则表达式:提取 ->xxx 格式的行并转换为 #define
define sed-offsets
	"s:[[:space:]]*\.ascii[[:space:]]*\"\(.*\)\":\1:; \
	/^->/{s:->#\(.*\):/* \1 */:; \
	s:^->\([^ ]*\) [\$$#]*\([-0-9]*\) \(.*\):#define \1 \2 /* \3 */:; \
	s:^->\([^ ]*\) [\$$#]*\([^ ]*\) \(.*\):#define \1 \2 /* \3 */:; \
	s:->::; p;}"
endef

# filechk_offsets: 生成完整的头文件
# $2 是头文件保护宏名称
define filechk_offsets
	(set -e; \
	 echo "#ifndef $2"; \
	 echo "#define $2"; \
	 echo "/*"; \
	 echo " * DO NOT MODIFY."; \
	 echo " *"; \
	 echo " * This file was generated by Kbuild"; \
	 echo " */"; \
	 echo ""; \
	 sed -ne $(sed-offsets); \
	 echo ""; \
	 echo "#endif" )
endef

6. Kbuild.include

Kbuild.include 是Kbuild 的通用定义文件。

# 基础字符常量
comma   := ,
quote   := "
squote  := '
empty   :=
space   := $(empty) $(empty)
space_escape := _-_SPACE_-_
pound   := \#

# 路径和文件名处理
# dot-target: 目标文件的隐藏版本路径
# foo/bar.o => foo/.bar.o
# 用于生成 .cmd 文件路径
dot-target = $(dir $@).$(notdir $@)

# depfile: 依赖文件路径(替换逗号避免问题)
# 用于 -Wp,-MD,$(depfile)
depfile = $(subst $(comma),_,$(dot-target).d)

# basetarget: 不带目录和扩展名的目标名
# 用于 CFLAGS_$(basetarget).o 等
basetarget = $(basename $(notdir $@))

# escsq: 转义单引号,用于 echo 语句
escsq = $(subst $(squote),'\$(squote)',$1)


# 状态消息输出
       kecho := :
 quiet_kecho := echo
silent_kecho := :
kecho := $($(quiet)kecho)

# filechk 函数 【保留 - 用于生成 version.h、timestamp.h、uboot.release】
# =========================================================================
# 检查生成文件内容是否需要更新
# 只有内容真正改变时才更新文件,避免不必要的重编译
define filechk
	$(Q)set -e;				\
	mkdir -p $(dir $@);			\
	$(filechk_$(1)) < $< > $@.tmp;		\
	if [ -r $@ ] && cmp -s $@ $@.tmp; then	\
		rm -f $@.tmp;			\
	else					\
		$(kecho) '  UPD     $@';	\
		mv -f $@.tmp $@;		\
	fi
endef

# 编译器选项探测函数

# 输出目录(用于临时文件)
TMPOUT :=

# try-run: 尝试运行命令,根据成功/失败返回不同值
# 用于探测编译器是否支持某选项
try-run = $(shell set -e;		\
	TMP="$(TMPOUT).$$$$.tmp";	\
	TMPO="$(TMPOUT).$$$$.o";	\
	TMPSU="$(TMPOUT).$$$$.su";	\
	if ($(1)) >/dev/null 2>&1;	\
	then echo "$(2)";		\
	else echo "$(3)";		\
	fi;				\
	rm -f "$$TMP" "$$TMPO" "$$TMPSU")

# as-option: 检测汇编器选项
as-option = $(call try-run,\
	$(CC) $(KBUILD_CFLAGS) $(1) -c -x assembler /dev/null -o "$$TMP",$(1),$(2))

# __cc-option: 通用编译器选项检测
__cc-option = $(call try-run,\
	$(1) -Werror $(2) $(3) -c -x c /dev/null -o "$$TMP",$(3),$(4))

# cc-option: 检测 C 编译器选项
cc-option = $(call __cc-option, $(CC),\
	$(KBUILD_CPPFLAGS) $(KBUILD_CFLAGS),$(1),$(2))

# cc-name: 获取编译器名称(gcc 或 clang)
cc-name = $(shell $(CC) -v 2>&1 | grep -q "clang version" && echo clang || echo gcc)

# cc-version: 获取编译器版本
cc-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/gcc-version.sh $(CC))

# added for U-Boot
binutils-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/binutils-version.sh $(AS))
dtc-version = $(shell $(CONFIG_SHELL) $(srctree)/scripts/dtc-version.sh $(DTC))

# build: 递归构建简写
# 用法: $(Q)$(MAKE) $(build)=dir
# 展开: @make -f scripts/Makefile.build obj=dir
build := -f $(srctree)/scripts/Makefile.build obj

###
# clean: 递归清理简写
# 用法: $(Q)$(MAKE) $(clean)=dir
# 展开: @make -f scripts/Makefile.clean obj=dir
clean := -f $(srctree)/scripts/Makefile.clean obj

# addtree: 将相对路径的 -I 选项加上 $(srctree) 前缀
# 用于外部构建时定位源码树中的头文件
# 例如: -Iinclude -> -I$(srctree)/include
# 但 -I/usr/include 保持不变(绝对路径)
addtree = $(if $(patsubst -I%,%,$(1)), \
$(if $(filter-out -I/% -I./% -I../%,$(1)),$(patsubst -I%,-I$(srctree)/%,$(1)),$(1)),$(1))

# flags: 处理变量中所有 -I 选项
# 对每个 -I 选项调用 addtree,其他选项保持不变
flags = $(foreach o,$($(1)),$(if $(filter -I%,$(o)),$(call addtree,$(o)),$(o)))

# echo-cmd: 根据 $(quiet) 变量选择输出格式
# quiet_ 前缀: 简短输出(如 "CC foo.o")
# 无前缀: 完整命令输出
# silent_ 前缀: 无输出
echo-cmd = $(if $($(quiet)cmd_$(1)),\
	echo '  $(call escsq,$($(quiet)cmd_$(1)))$(echo-why)';)

# cmd: 执行命令的简写
# @$(echo-cmd): 先输出命令描述(根据 V= 设置)
# $(cmd_$(1)): 再执行实际命令
cmd = @$(echo-cmd) $(cmd_$(1))

# arg-check: 检查命令是否改变
# 比较上次保存的命令 (cmd_$@) 与当前命令 (cmd_$1)
# 空字符串表示相同,非空表示有变化
ifneq ($(KBUILD_NOCMDDEP),1)
# 正常模式:完整比较命令(包括参数顺序)
arg-check = $(filter-out $(subst $(space),$(space_escape),$(strip $(cmd_$@))), \
                         $(subst $(space),$(space_escape),$(strip $(cmd_$1))))
else
# KBUILD_NOCMDDEP=1: 只检查命令是否存在
arg-check = $(if $(strip $(cmd_$@)),,1)
endif

# make-cmd: 准备保存到 .cmd 文件的命令字符串
# 转义 $, #, ' 等特殊字符
make-cmd = $(call escsq,$(subst $(pound),$$(pound),$(subst $$,$$$$,$(cmd_$(1)))))

# any-prereq: 检查是否有比目标新的依赖,或依赖不存在
# $?: 比目标新的依赖列表
# $^: 所有依赖列表
# 过滤掉 PHONY 目标
any-prereq = $(filter-out $(PHONY),$?) $(filter-out $(PHONY) $(wildcard $^),$^)

# if_changed: 条件执行命令
# 当命令改变或依赖更新时才执行
# 执行后保存命令到 .cmd 文件
if_changed = $(if $(strip $(any-prereq) $(arg-check)),                       \
	@set -e;                                                             \
	$(echo-cmd) $(cmd_$(1));                                             \
	printf '%s\n' 'cmd_$@ := $(make-cmd)' > $(dot-target).cmd, @:)

# if_changed_dep: 条件执行 + 处理依赖文件
# 执行命令后用 fixdep 处理 .d 文件
# 用于编译 C/汇编文件
if_changed_dep = $(if $(strip $(any-prereq) $(arg-check) ),                  \
	@set -e;                                                             \
	$(echo-cmd) $(cmd_$(1));                                             \
	scripts/basic/fixdep $(depfile) $@ '$(make-cmd)' > $(dot-target).tmp;\
	rm -f $(depfile);                                                    \
	mv -f $(dot-target).tmp $(dot-target).cmd, @:)

# if_changed_rule: 条件执行规则(而非单个命令)
# 用于需要多步骤的复杂规则
if_changed_rule = $(if $(strip $(any-prereq) $(arg-check) ),                 \
	@set -e;                                                             \
	$(rule_$(1)), @:)

# 调试输出(V=2 时启用)
ifeq ($(KBUILD_VERBOSE),2)
why =                                                                        \
    $(if $(filter $@, $(PHONY)),- due to target is PHONY,                    \
        $(if $(wildcard $@),                                                 \
            $(if $(strip $(any-prereq)),- due to: $(any-prereq),             \
                $(if $(arg-check),                                           \
                    $(if $(cmd_$@),- due to command line change,             \
                        $(if $(filter $@, $(targets)),                       \
                            - due to missing .cmd file,                      \
                            - due to $(notdir $@) not in $$(targets)         \
                         )                                                   \
                     )                                                       \
                 )                                                           \
             ),                                                              \
             - due to target missing                                         \
         )                                                                   \
     )

echo-why = $(call escsq, $(strip $(why)))
endif

7. Makefile.host

在宿主机系统上构建二进制工具

这些工具在编译 U-Boot/内核 过程中使用,例如:

  • fixdep: 处理依赖文件
  • conf/mconf: Kconfig 配置工具
  • mkimage: 生成 U-Boot 镜像
  • bin2hex: 数据格式转换
# host-csingle: 单文件 C 程序
# 判断逻辑:如果程序没有定义 -objs、-cxxobjs、-sharedobjs,则是单文件程序
# foreach 遍历每个程序 m:
#   $($(m)-objs)$($(m)-cxxobjs)$($(m)-sharedobjs):展开该程序的所有组件定义
#   $(if ...):如果都为空,则返回 $(m),否则返回空
host-csingle	:= $(foreach m,$(__hostprogs), \
			$(if $($(m)-objs)$($(m)-cxxobjs)$($(m)-sharedobjs),,$(m)))

# host-cmulti: 多文件 C 程序(纯 C,不含 C++)
# 判断逻辑:有 -objs 定义,但没有 -cxxobjs 定义
host-cmulti	:= $(foreach m,$(__hostprogs),\
		   $(if $($(m)-cxxobjs),,$(if $($(m)-objs),$(m))))
# host-cobjs: 所有需要编译的 C 目标文件
# 收集所有程序的 -objs 定义中的 .o 文件
# sort 去重(同一个 .o 可能被多个程序共享)
host-cobjs	:= $(sort $(foreach m,$(__hostprogs),$($(m)-objs)))

# 添加目录前缀
# =========================================================================
# 将所有目标添加 $(obj)/ 前缀,支持外部构建
# 例如:obj=scripts/kconfig
#   host-csingle = scripts/kconfig/conf
#   host-cobjs = scripts/kconfig/menu.o scripts/kconfig/zconf.tab.o
host-csingle	:= $(addprefix $(obj)/,$(host-csingle))
host-cmulti	:= $(addprefix $(obj)/,$(host-cmulti))
host-cobjs	:= $(addprefix $(obj)/,$(host-cobjs))

# 编译标志处理
# _hostc_flags: 基础宿主机 C 编译标志
# 组成部分:
#   KBUILD_HOSTCFLAGS  : 全局宿主机编译标志(在顶层 Makefile 定义)
#                        通常包含 -Wall -O2 等
#   HOST_EXTRACFLAGS   : 额外的宿主机标志(可在 Kbuild 文件中设置)
#   HOSTCFLAGS_xxx.o   : 特定文件的编译标志
#                        例如:HOSTCFLAGS_zconf.tab.o := -I$(src)
_hostc_flags   = $(KBUILD_HOSTCFLAGS)   $(HOST_EXTRACFLAGS)   \
                 $(HOSTCFLAGS_$(basetarget).o)
# 外部构建支持
ifeq ($(KBUILD_SRC),)
# 源码内构建:直接使用标志
__hostc_flags	= $(_hostc_flags)
else
# 外部构建:添加 -I$(obj) 以找到生成的头文件
# flags 函数(定义在 Kbuild.include)处理 -I 路径转换
__hostc_flags	= -I$(obj) $(call flags,_hostc_flags)
endif

# hostc_flags: 最终编译标志
# -Wp,-MD,$(depfile): 生成依赖文件
#   -Wp: 将后续选项传给预处理器
#   -MD: 生成 .d 依赖文件
# $(__hostc_flags): 上面处理过的编译标志
hostc_flags    = -Wp,-MD,$(depfile) $(__hostc_flags)

# 编译规则
# 规则 1: 单文件 C 程序 (host-csingle)
# 示例展开(编译 scripts/basic/fixdep):
#   cc -Wp,-MD,.fixdep.d -Wall -O2 -o scripts/basic/fixdep \
#      scripts/basic/fixdep.c
quiet_cmd_host-csingle 	= HOSTCC  $@
      cmd_host-csingle	= $(HOSTCC) $(hostc_flags) $(KBUILD_HOSTLDFLAGS) -o $@ $< \
	  	$(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(@F))

# 模式规则:
#   $(obj)/%: $(src)/%.c  表示:
#     目标 scripts/basic/fixdep 依赖于 scripts/basic/fixdep.c
# if_changed_dep:
#   1) 检查命令或依赖是否改变
#   2) 如果改变,执行命令并处理 .d 文件
$(host-csingle): $(obj)/%: $(src)/%.c FORCE
	$(call if_changed_dep,host-csingle)

# 规则 2: 多文件 C 程序链接 (host-cmulti)
# 输入:多个 .o 文件(由 host-cobjs 规则生成)
# 输出:可执行文件
quiet_cmd_host-cmulti	= HOSTLD  $@
      cmd_host-cmulti	= $(HOSTCC) $(KBUILD_HOSTLDFLAGS) -o $@ \
			  $(addprefix $(obj)/,$($(@F)-objs)) \
			  $(KBUILD_HOSTLDLIBS) $(HOSTLDLIBS_$(@F))

# 因为依赖由 multi_depend 动态生成
$(host-cmulti): FORCE
	$(call if_changed,host-cmulti)
	$(call multi_depend, $(host-cmulti), , -objs)

# 规则 3: C 目标文件编译 (host-cobjs)
# 这是 host-cmulti 的前置步骤,先编译所有 .o 文件
quiet_cmd_host-cobjs	= HOSTCC  $@
      cmd_host-cobjs	= $(HOSTCC) $(hostc_flags) -c -o $@ $<

$(host-cobjs): $(obj)/%.o: $(src)/%.c FORCE
	$(call if_changed_dep,host-cobjs)

# 将所有宿主机目标加入 targets 列表
# 用于:
#   1) clean 时清理这些文件
#   2) if_changed 的目标验证(V=2 调试信息)
targets += $(host-csingle) $(host-cmulti) $(host-cobjs)

8. Makefile.autoconf

此辅助 Makefile 用于创建:

  • 符号链接 (arch/$ARCH/include/asm/arch -> arch-$(SOC))
  • include/autoconf.mk 及其依赖文件
  • include/config.h

在 Kconfig 引入之前,这些文件由 mkconfig 脚本生成,现在由此 Makefile 处理。这个 Makefile 是连接 Kconfig 系统和实际编译的桥梁,确保配置信息以正确的格式提供给 Makefile 和 C 代码。

# 默认目标:生成 autoconf.mk 和其依赖文件
__all: include/autoconf.mk include/autoconf.mk.dep

# 读取 Kconfig 生成的配置(CONFIG_xxx=y 等)
# 这是 syncconfig 的输出,包含所有启用的配置选项
include include/config/auto.conf

# 包含 Kbuild 通用函数(filechk、cmd 等)
include scripts/Kbuild.include

# 需要在这里重新定义 CC 和 CPP,因为:
# 1) 此 Makefile 可能被独立调用(不经过顶层 Makefile)
# 2) 某些架构在 arch/$(ARCH)/config.mk 中定义 CROSS_COMPILE
#
# CC:  C 编译器
# CPP: C 预处理器(仅预处理,不编译)
CC		= $(CROSS_COMPILE)gcc
CPP		= $(CC) -E

# 包含板级配置(定义 ARCH, CPU, BOARD, VENDOR, SOC 等变量)
# 这些变量用于确定符号链接的目标路径
include config.mk

# 头文件搜索路径
# 用于编译和预处理时的 -I 选项
UBOOTINCLUDE    := \
		-Iinclude \
		$(if $(KBUILD_SRC), -I$(srctree)/include) \
		-I$(srctree)/arch/$(ARCH)/include

# 用于预处理 include/config.h 以提取 CONFIG_xxx 宏
c_flags := $(KBUILD_CFLAGS) $(KBUILD_CPPFLAGS) $(PLATFORM_CPPFLAGS) \
					$(UBOOTINCLUDE) $(NOSTDINC_FLAGS)

# autoconf.mk.dep - 依赖文件生成
# 作用:跟踪 include/config.h 的所有依赖关系
# 当任何被包含的头文件改变时,触发 autoconf.mk 重新生成
# 命令解析:
#   -x c               : 强制作为 C 语言处理
#   -DDO_DEPS_ONLY     : 定义此宏(某些头文件据此跳过实际内容)
#   -M                 : 生成 make 依赖规则(只输出依赖,不编译)
#   -MP                : 为每个依赖生成空规则(防止头文件删除后报错)
#   -MQ target         : 指定依赖规则的目标名(这里是 auto.conf)
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;							\
	}

# 依赖规则:当 config.h 改变时重新生成
# FORCE 确保每次 make 都检查是否需要更新
include/autoconf.mk.dep: include/config.h FORCE
	$(call cmd,autoconf_dep)

# autoconf.mk - 传统格式的配置文件
# 作用:将 C 预处理器宏格式转换为 Makefile 变量格式
# 输入(u-boot.cfg 中的内容):
#   #define CONFIG_SYS_TEXT_BASE 0x87800000
#   #define CONFIG_ARM y
# 输出(autoconf.mk):
#   CONFIG_SYS_TEXT_BASE=0x87800000
#   CONFIG_ARM=y
# 处理逻辑:
#   1) sed 使用 define2mk.sed 脚本转换格式
#   2) while 循环检查重复定义(可通过 KCONFIG_IGNORE_DUPLICATES 跳过)
#   3) 只输出在 auto.conf 中未定义的选项(避免冲突)
# 这种双重检查的原因:
#   - Kconfig 生成的选项在 auto.conf 中(权威来源)
#   - 头文件中的传统 #define 在 autoconf.mk 中(兼容旧代码)
#   - 避免同一选项被定义两次导致的警告或错误
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/config.h 中所有 CONFIG_xxx 宏定义
# 命令解析:
#   $(CPP) ... -dM    : 预处理并输出所有宏定义(-dM = dump macros)
#   grep 'define CONFIG_' : 只保留 CONFIG_ 开头的宏
#   sed 过滤掉:
#     - CONFIG_IS_ENABLED()  : 这是宏函数,不是配置值
#     - CONFIG_IF_ENABLED_INT() : 同上
#     - CONFIG_VAL()         : 同上
# 输出示例:
#   #define CONFIG_ARM 1
#   #define CONFIG_SYS_TEXT_BASE 0x87800000
#   #define CONFIG_BAUDRATE 115200
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;					\
	}

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

# autoconf.mk 依赖于 u-boot.cfg(转换格式)
include/autoconf.mk: u-boot.cfg
	$(call cmd,autoconf)

# include/config.h - 核心配置头文件
# 作用:C 代码的配置入口点,所有 .c/.h 文件通过它访问配置
# 生成内容:
#   /* Automatically generated - do not edit */
#   #define CFG_BOARDDIR board/freescale/mx6ullevk
#   #include <configs/mx6ullevk.h>      // 板级配置头文件
#   #include <asm/config.h>             // 架构配置
#   #include <linux/kconfig.h>          // Kconfig 生成的配置
#
# 设计说明:
#   - CFG_BOARDDIR: 板级目录路径,用于定位板级特定文件
#   - configs/xxx.h: 传统的板级配置(正在逐步迁移到 Kconfig)
#   - asm/config.h: 架构级默认配置
#   - linux/kconfig.h: 包含 auto.conf 的 C 头文件版本
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\>;)
endef

# config.h 的依赖:
#   - 此 Makefile 本身(规则改变时重新生成)
#   - create_symlink(确保 asm/arch 链接存在)
#   - FORCE(总是检查是否需要更新)
#
# filechk 宏(定义在 Kbuild.include)会:
#   1) 生成新内容到临时文件
#   2) 与现有文件比较
#   3) 仅在内容改变时才更新(避免不必要的重编译)
include/config.h: scripts/Makefile.autoconf create_symlink FORCE
	$(call filechk,config_h)

# 架构符号链接创建
# 作用:创建 arch/$(ARCH)/include/asm/arch 符号链接
# 为什么需要这个链接?
#   - 代码中使用 #include <asm/arch/xxx.h> 的统一写法
#   - 实际指向 arch/arm/include/asm/arch-mx6/ 等 SoC 特定目录
#   - 这样切换 SoC 时只需改变符号链接,无需修改代码
#
# 链接目标选择逻辑:
#   1) 优先检查 arch/$(ARCH)/mach-$(SOC)/include/mach/ 是否存在
#      某些架构(如 ARM 的 mach-xxx)使用这种布局
#   2) 否则使用 arch/$(ARCH)/include/asm/arch-$(SOC)/
#      这是更常见的布局(如 arch-mx6, arch-sunxi)
#   3) 如果 SOC 未定义,退化使用 CPU(如 arch-armv7)
#
# 外部构建(KBUILD_SRC)与源码内构建的区别:
#   - 外部构建:链接需要指向 $(KBUILD_SRC)/arch/... 的绝对路径
#   - 源码内构建:使用相对路径 ../../arch-xxx

PHONY += create_symlink
create_symlink:
# CONFIG_CREATE_ARCH_SYMLINK 控制是否创建链接
# 某些架构(如 sandbox)不需要此链接
ifdef CONFIG_CREATE_ARCH_SYMLINK
ifneq ($(KBUILD_SRC),)
	# ----- 外部构建模式 -----
	# 首先确保 include/asm 目录存在
	$(Q)mkdir -p include/asm
	# 确定链接目标并创建链接
	$(Q)if [ -d $(KBUILD_SRC)/arch/$(ARCH)/mach-$(SOC)/include/mach ]; then	\
		dest=arch/$(ARCH)/mach-$(SOC)/include/mach;			\
	else									\
		dest=arch/$(ARCH)/include/asm/arch-$(if $(SOC),$(SOC),$(CPU));	\
	fi;									\
	ln -fsn $(KBUILD_SRC)/$$dest include/asm/arch
	# -fsn 选项:
	#   -f: 强制覆盖已存在的链接
	#   -s: 创建符号链接(而非硬链接)
	#   -n: 如果目标是目录的链接,不要进入它
else
	# ----- 源码内构建模式 -----
	# 使用相对路径创建链接
	$(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
	# 相对路径说明:
	# 从 arch/arm/include/asm/arch 出发:
	#   ../../mach-imx/include/mach  → arch/arm/mach-imx/include/mach
	#   arch-mx6                      → arch/arm/include/asm/arch-mx6 (同级目录)
endif
endif

# 标准伪目标声明
PHONY += FORCE
FORCE:

.PHONY: $(PHONY)

9. Makefile.clean

这是清理系统,用于清理构建产物。自动发现并清理所有子目录,包括禁用配置的目录(可能之前编译过),只删除已知的构建产物,不会误删源文件。

# 功能:
#   1) 删除当前目录下的构建产物(.o, .cmd, 生成的文件等)
#   2) 递归进入子目录执行清理
# src 与 obj 相同(清理时源码目录 = 输出目录的对应关系)
src := $(obj)

# 默认目标
PHONY := __clean
__clean:

# 包含通用函数(cmd, clean 简写等)
include scripts/Kbuild.include

# 读取目标目录的 Kbuild/Makefile 文件
# 需要知道该目录定义了哪些目标,才能知道要清理什么
#
# kbuild-dir: 确定 Kbuild 文件的完整路径
#   如果 src 是绝对路径(以 / 开头),直接使用,否则加上 $(srctree)/ 前缀
kbuild-dir := $(if $(filter /%,$(src)),$(src),$(srctree)/$(src))

# 优先使用 Kbuild 文件,其次使用 Makefile
include $(if $(wildcard $(kbuild-dir)/Kbuild), $(kbuild-dir)/Kbuild, $(kbuild-dir)/Makefile)

# 从 Kbuild 变量中提取需要处理的内容
# 提取子目录列表

# __subdir-y: 从 obj-y 中提取子目录(以 '/' 结尾的项)
__subdir-y	:= $(patsubst %/,%,$(filter %/, $(obj-y)))
subdir-y	+= $(__subdir-y)

# __subdir-: 从 obj- 中提取子目录
# obj- 是被禁用的目标(CONFIG_xxx=n 时),这些目录也需要清理(可能之前编译过,现在禁用了)
__subdir-	:= $(patsubst %/,%,$(filter %/, $(obj-)))
subdir-		+= $(__subdir-)

# 合并需要递归清理的子目录
# subdir-ym: 启用的子目录(去重排序)
subdir-ym	:= $(sort $(subdir-y))

# subdir-ymn: 所有子目录(启用的 + 禁用的)
# 清理时需要处理所有目录,不管当前是否启用
subdir-ymn      := $(sort $(subdir-ym) $(subdir-))

# 添加目录前缀
subdir-ymn	:= $(addprefix $(obj)/,$(subdir-ymn))

# U-Boot 临时解决方案:过滤不存在的目录
# 某些目录可能在 Kbuild 文件中声明,但实际并不存在(可能是条件编译导致的,或者是从其他项目复制的配置)
# 这里检查每个目录是否真的有 Makefile,没有则跳过
#
# 注意:使用 $(srctree)/$f/Makefile 检查源码树中的文件
subdir-ymn	:= $(foreach f, $(subdir-ymn), \
				$(if $(wildcard $(srctree)/$f/Makefile),$f))

# 构建要清理的文件列表
# __clean-files: 收集所有需要删除的文件
# 来源包括:
#   extra-y, extra-      : 额外的构建目标
#   always               : 始终构建的目标
#   targets              : 在 Makefile.build 中注册的目标
#   clean-files          : 显式声明需要清理的文件
#   hostprogs-y/hostprogs- : 宿主机程序
#   hostlibs-y/hostlibs- : 宿主机静态库
#   hostcxxlibs-y        : 宿主机 C++ 库
__clean-files	:= $(extra-y) $(extra-)       \
		   $(always) $(targets) $(clean-files)   \
		   $(hostprogs-y) $(hostprogs-) \
		   $(hostlibs-y) $(hostlibs-) \
		   $(hostcxxlibs-y)

# 过滤掉不需要清理的文件
# no-clean-files: 在 Kbuild 文件中可以定义此变量,列出虽然是生成文件但不应被 clean 删除的文件
# 例如:某些需要手动维护的生成文件
__clean-files   := $(filter-out $(no-clean-files), $(__clean-files))

# 处理文件路径
# 文件路径有两种情况:
#   1) 相对路径:相对于当前目录($(obj)/)
#   2) 绝对路径:以 $(objtree)/ 开头(指向输出目录根)
#
# 处理逻辑:
#   - filter-out $(objtree)/%: 提取相对路径,加上 $(obj)/ 前缀
#   - filter $(objtree)/%: 提取绝对路径,保持不变
#   - wildcard: 只保留实际存在的文件(避免 rm 报错)
# 示例:
#   obj = scripts/kconfig
#   __clean-files = conf mconf zconf.tab.c $(objtree)/include/config.h
#
#   处理后:
#   - scripts/kconfig/conf(如果存在)
#   - scripts/kconfig/mconf(如果存在)
#   - scripts/kconfig/zconf.tab.c(如果存在)
#   - ./include/config.h(如果存在)
__clean-files   := $(wildcard                                               \
		   $(addprefix $(obj)/, $(filter-out $(objtree)/%, $(__clean-files))) \
		   $(filter $(objtree)/%, $(__clean-files)))

# clean-dirs: 在 Kbuild 文件中定义需要删除的整个目录
# 例如:clean-dirs := generated/
__clean-dirs    := $(wildcard                                               \
		   $(addprefix $(obj)/, $(filter-out $(objtree)/%, $(clean-dirs)))    \
		   $(filter $(objtree)/%, $(clean-dirs)))

# cmd_clean: 删除文件
# 使用 rm -f(-f 忽略不存在的文件,不报错)
quiet_cmd_clean    = CLEAN   $(obj)
      cmd_clean    = rm -f $(__clean-files)

# cmd_cleandir: 删除目录
# 使用 rm -rf(递归删除整个目录)
quiet_cmd_cleandir = CLEAN   $(__clean-dirs)
      cmd_cleandir = rm -rf $(__clean-dirs)

# 主清理规则
# 执行顺序:
#   1) 递归进入所有子目录($(subdir-ymn))
#   2) 删除当前目录的文件(如果有)
#   3) 删除当前目录的子目录(如果有)
#
# 依赖 $(subdir-ymn) 确保:先清理子目录,再清理当前目录
# 这样如果当前目录要删除某个子目录,它已经是空的了

__clean: $(subdir-ymn)
# 如果有文件需要删除
ifneq ($(strip $(__clean-files)),)
	# '+' 前缀:即使 make -n (dry-run) 也执行此命令
	# 原因:清理操作通常是安全的,用户期望看到效果
	+$(call cmd,clean)
endif
# 如果有目录需要删除
ifneq ($(strip $(__clean-dirs)),)
	+$(call cmd,cleandir)
endif
	# @: 是空命令(no-op)
	# 确保规则总是有命令,即使上面两个 ifneq 都不满足
	# 没有命令的规则可能导致 make 行为异常
	@:

# 子目录递归
PHONY += $(subdir-ymn)

# 对每个子目录,递归调用 Makefile.clean
# $(clean) 在 Kbuild.include 中定义:
#   clean := -f $(srctree)/scripts/Makefile.clean obj
#
# 展开示例:
#   make -f scripts/Makefile.clean obj=drivers/gpio
$(subdir-ymn):
	$(Q)$(MAKE) $(clean)=$@

.PHONY: $(PHONY)

10. include/host_arch.h

它是一个 Makefile 和 C 语言共用的头文件

  • # 开头的行对 Makefile 来说是注释。
  • C 预处理器看到 #if 0,直接跳到 #endif,忽略中间内容。

export HOST_ARCH_AARCH64=0xaa64是有效的 Makefile 语句。

#include 和 #define 是 C 预处理指令,Makefile 看到 # 开头,全部当作注释忽略。

#if 0
# SPDX SPDX-License-Identifier: GPL-2.0+
#
# Constants defining the host architecture in assembler, C, and make files.
# The values are arbitrary.
#
# Copyright 2019 Heinrich Schuchardt <xypron.glpk@gmx.de>
#endif

#if 0
export HOST_ARCH_AARCH64=0xaa64
export HOST_ARCH_ARM=0x00a7
export HOST_ARCH_RISCV32=0x5032
export HOST_ARCH_RISCV64=0x5064
export HOST_ARCH_X86=0x0386
export HOST_ARCH_X86_64=0x8664
#endif

#include <version.h>

#define HOST_ARCH_AARCH64 0xaa64
#define HOST_ARCH_ARM 0x00a7
#define HOST_ARCH_RISCV32 0x5032
#define HOST_ARCH_RISCV64 0x5064
#define HOST_ARCH_X86 0x0386
#define HOST_ARCH_X86_64 0x8664

11. config.mk

# SPDX-License-Identifier: GPL-2.0+
#
# (C) Copyright 2000-2013
# Wolfgang Denk, DENX Software Engineering, wd@denx.de.
#########################################################################

# This file is included from ./Makefile and spl/Makefile.
# Clean the state to avoid the same flags added twice.
#
# (Tegra needs different flags for SPL.
#  That's the reason why this file must be included from spl/Makefile too.
#  If we did not have Tegra SoCs, build system would be much simpler...)
PLATFORM_RELFLAGS :=
PLATFORM_CPPFLAGS :=
LDFLAGS_FINAL :=
LDFLAGS_STANDALONE :=
OBJCOPYFLAGS :=
# clear VENDOR for tcsh
VENDOR :=
#########################################################################

ARCH := $(CONFIG_SYS_ARCH:"%"=%)
CPU := $(CONFIG_SYS_CPU:"%"=%)
BOARD := $(CONFIG_SYS_BOARD:"%"=%)
ifneq ($(CONFIG_SYS_VENDOR),)
VENDOR := $(CONFIG_SYS_VENDOR:"%"=%)
endif
ifneq ($(CONFIG_SYS_SOC),)
SOC := $(CONFIG_SYS_SOC:"%"=%)
endif

# Some architecture config.mk files need to know what CPUDIR is set to,
# so calculate CPUDIR before including ARCH/SOC/CPU config.mk files.
# Check if arch/$ARCH/cpu/$CPU exists, otherwise assume arch/$ARCH/cpu contains
# CPU-specific code.
CPUDIR=arch/$(ARCH)/cpu$(if $(CPU),/$(CPU),)

sinclude $(srctree)/arch/$(ARCH)/config.mk	# include architecture dependend rules
sinclude $(srctree)/$(CPUDIR)/config.mk		# include  CPU	specific rules

ifdef	SOC
sinclude $(srctree)/$(CPUDIR)/$(SOC)/config.mk	# include  SoC	specific rules
endif
ifneq ($(BOARD),)
ifdef	VENDOR
BOARDDIR = $(VENDOR)/$(BOARD)
ENVDIR=${vendor}/env
else
BOARDDIR = $(BOARD)
ENVDIR=${board}/env
endif
endif
ifdef	BOARD
sinclude $(srctree)/board/$(BOARDDIR)/config.mk	# include board specific rules
endif

ifdef FTRACE
PLATFORM_CPPFLAGS += -finstrument-functions -DFTRACE
endif

#########################################################################

RELFLAGS := $(PLATFORM_RELFLAGS)

PLATFORM_CPPFLAGS += $(RELFLAGS)
PLATFORM_CPPFLAGS += -pipe

LDFLAGS_FINAL += -Bstatic

export PLATFORM_CPPFLAGS
export RELFLAGS
export LDFLAGS_FINAL
export LDFLAGS_STANDALONE
export CONFIG_STANDALONE_LOAD_ADDR

12.编译辅助程序移植

(1)fixdep

直接将fixdep.c源文件复制到scripts/basic/下

scripts/basic/Makefile

# SPDX-License-Identifier: GPL-2.0
###
# Makefile.basic 列出了构建过程中使用的基本程序。
# 此处列出的程序用于完成基础操作,例如修复文件依赖关系。
# 此初始步骤旨在避免内核配置变更时(当主 Makefile 包含 .config 文件时即会发生此情况)文件被重新编译。
# ---------------------------------------------------------------------------
# fixdep:     用于在构建过程中生成依赖关系信息

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

# fixdep 是编译其他主机程序所必需的
$(addprefix $(obj)/,$(filter-out fixdep,$(always))): $(obj)/fixdep

(2)kconfig

将U-Boot源码中的kconfig目录下的文件全部复制到scripts/kconfig/目录下。

也可以按照我的方式仅保留核心部分:

kconfig

lxdialog/目录内容全部复制。

kconfig/Makefile内容调整:

# SPDX-License-Identifier: GPL-2.0
# ===========================================================================
# Kernel configuration targets
# These targets are used from top-level makefile

PHONY += menuconfig config localyesconfig build_menuconfig

# Added for U-Boot
#  Linux has defconfig files in arch/$(SRCARCH)/configs/,
#  on the other hand, U-Boot does in configs/.
#  Set SRCARCH to .. fake this Makefile.
SRCARCH := ..

ifdef KBUILD_KCONFIG
Kconfig := $(KBUILD_KCONFIG)
else
Kconfig := Kconfig
endif

ifeq ($(quiet),silent_)
silent := -s
endif

# We need this, in case the user has it in its environment
unexport CONFIG_

menuconfig: $(obj)/mconf
	$< $(silent) $(Kconfig)

config: $(obj)/conf
	$< $(silent) --oldaskconfig $(Kconfig)

build_menuconfig: $(obj)/mconf

simple-targets := oldconfig syncconfig
PHONY += $(simple-targets)
$(simple-targets): $(obj)/conf
	$< $(silent) --$@ $(Kconfig)

PHONY += savedefconfig

savedefconfig: $(obj)/conf
	$< $(silent) --$@=defconfig $(Kconfig)

%_defconfig: $(obj)/conf
	$(Q)$(CPP) -nostdinc -P -I $(srctree) -undef -x assembler-with-cpp $(srctree)/arch/$(SRCARCH)/configs/$@ -o generated_defconfig
	$(Q)$< $(silent) --defconfig=generated_defconfig $(Kconfig)

# ===========================================================================
# Shared Makefile for the various kconfig executables:
# conf:	  Used for defconfig, oldconfig and related targets
# object files used by all kconfig flavours

conf-objs	:= conf.o  zconf.tab.o

hostprogs-y := conf

targets		+= zconf.lex.c

# generated files seem to need this to find local include files
HOSTCFLAGS_zconf.lex.o	:= -I$(src)
HOSTCFLAGS_zconf.tab.o	:= -I$(src)

# mconf: Used for the menuconfig target based on lxdialog
hostprogs-y	+= mconf
lxdialog	:= checklist.o inputbox.o menubox.o textbox.o util.o yesno.o
mconf-objs	:= mconf.o zconf.tab.o $(addprefix lxdialog/, $(lxdialog))

HOSTLDLIBS_mconf = $(shell . $(obj)/.mconf-cfg && echo $$libs)
$(foreach f, mconf.o $(lxdialog), \
  $(eval HOSTCFLAGS_$f = $$(shell . $(obj)/.mconf-cfg && echo $$$$cflags)))

$(obj)/mconf.o: $(obj)/.mconf-cfg
$(addprefix $(obj)/lxdialog/, $(lxdialog)): $(obj)/.mconf-cfg

$(obj)/zconf.tab.o: $(obj)/zconf.lex.c

# check if necessary packages are available, and configure build flags
define filechk_conf_cfg
	$(CONFIG_SHELL) $<
endef

$(obj)/.%conf-cfg: $(src)/%conf-cfg.sh FORCE
	$(call filechk,conf_cfg)

clean-files += .*conf-cfg

scripts/Makefile

always		:= $(hostprogs-y)

# Let clean descend into subdirs
subdir-	+= basic kconfig dtc

(3)Kconfig.include

# Kconfig helper macros

# Convenient variables
comma       := ,
quote       := "
squote      := '
empty       :=
space       := $(empty) $(empty)
dollar      := $
right_paren := )
left_paren  := (

# $(if-success,<command>,<then>,<else>)
# Return <then> if <command> exits with 0, <else> otherwise.
if-success = $(shell,{ $(1); } >/dev/null 2>&1 && echo "$(2)" || echo "$(3)")

# $(success,<command>)
# Return y if <command> exits with 0, n otherwise
success = $(if-success,$(1),y,n)

# $(cc-option,<flag>)
# Return y if the compiler supports <flag>, n otherwise
cc-option = $(success,$(CC) -Werror $(1) -E -x c /dev/null -o /dev/null)

# $(cc-define,<macro>)
# Return y if the compiler defines <macro>, n otherwise
cc-define = $(success,$(CC) -dM -E -x c /dev/null | grep -q '^#define \<$(1)\>')

# $(ld-option,<flag>)
# Return y if the linker supports <flag>, n otherwise
ld-option = $(success,$(LD) -v $(1))

# gcc version including patch level
gcc-version := $(shell,$(srctree)/scripts/gcc-version.sh -p $(CC) | sed 's/^0*//')

将下列各种相关文件直接复制到对应目录下:

scripts/gcc-version.sh
scripts/setlocalversion
scripts/mkmakefile

顶级目录下创建一个Kconfig文件

mainmenu "U-Boot $(UBOOTVERSION) Configuration"
comment "Compiler: $(CC_VERSION_TEXT)"

source "scripts/Kconfig.include"

menu "General setup"
endmenu # General setup

运行 make O=out menuconfig,可以看到配置图像画面出来。


13. 总结

至此Kconfig/Kbuild配置构建系统基本框架搭建完成。当前实现的版本是一个精简的核心框架,相比 U‑Boot 和 Linux 内核源码,去除了大量暂时用不到的功能(如多架构支持、复杂设备树编译、特定外设驱动等),但完整保留了 Kconfig 的配置机制、递归 Makefile 构建逻辑、主机工具编译支持以及自动依赖处理等核心模块。

所有后续开发都将以此框架为基础,随着项目功能的逐步增加,可以按需向系统中添加新的配置选项、子目录、编译规则以及主机工具,实现渐进式的功能扩展。

这样既大幅降低了初学者的理解门槛,又为后续的功能扩展提供了坚实的基石。


往期相关文章:

U-Boot 构建工具 fixdep 的工作原理以及编译分析

U-Boot 配置/构建系统(Kconfig/Kbuild)

U-Boot 的 Host 程序 make 流程

U-Boot配置编译过程及Makefile分析

Kconfig配置描述文件及问题记录

U-Boot编译过程的中间配置文件

IMX6ULL链接脚本u-boot.lds

include/generated/asm-offsets.h文件的生成过程

请作者喝杯茶吧~
微信打赏
微信
支付宝打赏
支付宝