View on GitHub

富乎 · 地问


avatar
辗转探寻为富乎?《天问》无解向地问!

<<< 返回主页

移植类项目的版本管理

1、背景

2、核心需求

  1. 少数改动的文件才是对自己项目有意义的,所以应该将这部分文件提取出来组成一个新项目( 后文将简称为新项目,同时将原来的组件称为基版)。

  2. 既然是一个新项目,就意味着应有自己单独的版本管理措施,即: 每次版本号的更新应当仅因新项目的文件变化而触发,并且版本号的生成结果也与基版无关。

  3. 被编译的源码必须是基版与新项目叠加后的结果。作为一名有追求的工程师, 叠加的操作肯定不能纯手工完成,而应该借助MakefileShell脚本, 否则这篇文章就不会出现。

  4. 若基版涉及图形化配置(典型例子是Linux内核的make menuconfig), 则改动后的配置内容要反向同步到新项目中。与前一点相同,也要借助MakefileShell脚本。

3、具体实现

U-BootLinux内核和基于Buildroot的根文件系统为例进行讲解(仅核心逻辑)。

3.1 U-Boot移植的版本管理

3.1.1 确定待改动的文件

假设目标项目采用NXP公司的i.MX6ULL为主控芯片,且基于其EVK样板进行改造, 则至少需要改以下文件(从这里开始,直接放Makefile内容):

# 若非指明,所有路径均是以基版项目(此处为U-Boot)根目录为起点。后同。
CUSTOM_FILES ?= configs/mx6ull_14x14_evk_nand_defconfig \
    include/configs/mx6ullevk.h \
    board/freescale/mx6ullevk/mx6ullevk.c

所以,新项目的目录结构类似这样:

新项目根目录
    |-- board
    |       `-- freescale
    |               `-- mx6ullevk
    |                       `-- mx6ullevk.c
    |-- configs
    |       `-- mx6ull_14x14_evk_nand_defconfig
    |-- include
    |       `-- configs
    |               `-- mx6ullevk.h
    `-- Makefile

3.1.2 提供版本号的生成方法

可按照以下思路去探索版本号的生成逻辑:

添加成功的版本号将编译进U-Boot二进制文件,在启动的时候会打印出来,类似这样:

U-Boot 2016.03-af0d234d1b4d (Feb 05 2024 - 11:05:18 +0800)

3.1.3 指定改动内容如何叠加到基版项目

Makefile依赖规则入手,让基版项目某一文件依赖于提取到新项目的同一文件即可,简单举例如下:

SRC_ROOT_DIR ?= /path/to/uboot

${SRC_ROOT_DIR}/a.c: a.c
    cp a.c ${SRC_ROOT_DIR}/a.c

这样,当新项目的a.c有变动时,在编译时会自动将其复制过去覆盖基版的a.c, 没改动则不会触发。如果有b.cc.c等多个文件,理论上可以一个一个地如法炮制,但只有新手才允许这么做, 换成老手就显得很“戆居”。因为,有经验的工程师至少应会使用模式规则自动变量, 那么上述规则无论来几个文件都可以用一条规则搞掂,一次编写,永久运行,不受文件增删的影响, 写法如下:

# CUSTOM_FILES是上一小节的待改动文件列表
${CUSTOM_FILES}: ${SRC_ROOT_DIR}/%: %
    cp $< $@

不过,方向虽然对了,但距离成功尚有一步之遥——因为上面的写法是错的……何解? 皆因%表示的是stem),在模式匹配中的位置可前而不可后(能放在最后的就叫做了, 但目前Makefile语法似乎不支持),所以需要另觅方案。经过多番尝试,得出一个个人认为较优雅又简单的写法, 就是利用多行变量eval函数(若无特殊处理,eval函数只能识别单行内容),写法如下:

# 先定义一个新的规则“函数”(Makefile里叫多行变量)
define custom_file_rule
${SRC_ROOT_DIR}/${1}: ${1}
    cp ${1} ${SRC_ROOT_DIR}/${1}
endef

# 再利用eval函数来对将要改动的文件列表逐个动态生成依赖规则
$(foreach i, ${CUSTOM_FILES}, $(eval $(call custom_file_rule,${i})))

当然,以上只是核心逻辑的展示,实际的脚本还会有比较diff)等操作, 以便直观显示出有哪些改动,有兴趣的可获取后文的链接去查阅全部内容。

3.1.4 自动保存更改后的编译配置(make menuconfig的结果)

直接用代码说话:

DEFCONFIG ?= configs/mx6ull_14x14_evk_nand_defconfig

defconfig: ${SRC_ROOT_DIR}/${DEFCONFIG}
    make $(notdir ${DEFCONFIG}) -C ${SRC_ROOT_DIR}

menuconfig: defconfig
    make menuconfig -C ${SRC_ROOT_DIR}
    cp ${SRC_ROOT_DIR}/.config ${DEFCONFIG}

有几点注意:

  1. 图形化配置通常不是从零开始,而是基于某个模板(这里是mx6ull_14x14_evk_nand_defconfig), 所以在make menuconfig之前会先make defconfig,而后者需要先应用改动后的模板,才能生成正确的.config文件。

  2. defconfig目标依赖于${SRC_ROOT_DIR}/${DEFCONFIG},根据前一小节已定义的叠加规则, 即可自动搬运有改动的模板文件到基版项目里进行覆盖。

  3. 以上仅展示核心逻辑来说明原理,完整版的内容还会有判断是否使用模板、 保存之前检查是否存在.config文件以及比较diff)等操作。

3.1.5 项目链接

3.2 Linux内核移植的版本管理

与前面的U-Boot思路及实现大同小异,但版本号的处理大不一样,因为内核的版本号不能随意增、删、改, 否则会影响到驱动程序的编译和诸多第三方程序或脚本的正常运行,所以需要另想办法。

首先在源码根目录的Makefile里找线索,最终找到一个KCFLAGS环境变量可供用户定制化, 即是说可通过该变量添加额外的编译选项,当然也包括新项目版本号的宏定义。其实, 这个变量在内核文档Documentation/kbuild/kbuild.txt(较新版文件名是kbuild.rst)也有正式说明, 可以放心使用。

接下来就要看能在什么地方安插自己的版本号。Linux用户都知道使用uname命令可查看系统版本号, 所以只需看它如何与内核交互,即可知道应该修改什么地方。通过阅读内核源码可知道, 与uname有关的procfs接口逻辑在init子目录,里面的init/version*.c使用若干个UTS_*宏来组成内核版本号的值, 继续追踪下去会发现UTS_SYSNAME(定义系统名称,例如Linux)、UTS_NODENAME(定义节点名称,约等于主机名)、 UTS_DOMAINNAME(定义域名,一般用不上,所以默认值为(none))支持定制化(默认值定义在include/linux/uts.h), 而UTS_RELEASE(发行版的主号,例如4.1.15)、UTS_VERSION(每次编译均会刷新, 含有编译次数、内核是否可抢占、编译时间等信息)、UTS_MACHINE(与架构相关,例如arm)则在编译期动态生成。 若用命令来查询这些宏的值则有:UTS_SYSNAME可用uname -sUTS_NODENAME可用uname -nUTS_DOMAINNAME可用cat /proc/sys/kernel/domainnameUTS_RELEASE可用uname -rUTS_VERSION可用uname -vUTS_MACHINE可用uname -m。综上,能被利用而不产生副作用的只有UTS_NODENAMEUTS_DOMAINNAMEUTS_VERSION。其中,前两个由于支持定制化, 所以可以利用前面的KCFLAGS环境变量传递两个可被gcc识别的-D选项值; 而UTS_VERSION虽然在名义上和形式上都是最合适的候选者,但因其值来源是编译期动态生成, 所以只有找出修改信息源的办法,才有可能把用户版本号注入其中。幸运的是, UTS_VERSION序号值(编译次数)是由KBUILD_BUILD_VERSION变量或根目录.version文件提供, 而前者支持用户赋值。所以,最终的版本号生成逻辑如下:

# __VER__和VCS变量由前面的__ver__.mk提供
export KBUILD_BUILD_VERSION ?= ${__VER__}
KCFLAGS ?= -DUTS_NODENAME=\\\"`hostname`[${__VER__}]\\\" -DUTS_DOMAINNAME=\\\"${VCS}://ver.${__VER__}.nil/\\\"

添加成功的版本号将编译进Linux镜像文件,在进入系统后可使用命令来查看,类似这样:

$ uname -v # 优先考虑这个命令
#5d2417e3f04a SMP PREEMPT Wed Feb 7 22:58:22 CST 2024
$
$ cat /proc/sys/kernel/domainname # 若上一个命令无效,则使用这个
git://ver.5d2417e3f04a.nil/
$
# “uname -n”通常会输出文件系统配置的主机名,覆盖掉编译时指定的宏定义

关于KBUILD_BUILD_VERSION变量还可以稍微展开说说。这个变量出现的位置, 在较旧版本(例如4.1.15)是在scripts/mkcompile_h,在较新版本则是init/Makefile, 并且在内核文档里无正式说明(不知是维护者懒得更新还是别的原因),让人用得不放心, 所以前面的版本号生成逻辑才同时使用UTS_NODENAMEUTS_DOMAINNAME,为了是多两道保险(实则只有一道), 万一将来某天内核维护者脑子抽风取消或修改掉这个变量,不至于没有地方存放用户版本号。

最后,给出完整内容的链接如下:

3.3 基于Buildroot的根文件系统制作的版本管理

与前面的U-Boot思路及实现大同小异,只需找出与版本相关的变量即可。照旧在根目录Makefile搜索VERSION碰碰运气, 发现了以下版本号逻辑:

 92 # Set and export the version string
 93 export BR2_VERSION := 2023.02
...
112 # Compute the full local version string so packages can use it as-is
113 # Need to export it, so it can be got from environment in children (eg. mconf)
114
115 BR2_LOCALVERSION := $(shell $(TOPDIR)/support/scripts/setlocalversion)
116 ifeq ($(BR2_LOCALVERSION),)
117 export BR2_VERSION_FULL := $(BR2_VERSION)
118 else
119 export BR2_VERSION_FULL := $(BR2_LOCALVERSION)
120 endif
...
720 .PHONY: target-finalize
721 target-finalize: $(PACKAGES) $(TARGET_DIR) host-finalize
...
757     mkdir -p $(TARGET_DIR)/etc
758     ( \
759         echo "NAME=Buildroot"; \
760         echo "VERSION=$(BR2_VERSION_FULL)"; \
761         echo "ID=buildroot"; \
762         echo "VERSION_ID=$(BR2_VERSION)"; \
763         echo "PRETTY_NAME=\"Buildroot $(BR2_VERSION)\"" \
764     ) >  $(TARGET_DIR)/usr/lib/os-release
765     ln -sf ../usr/lib/os-release $(TARGET_DIR)/etc

可知与BR2_VERSIONBR2_LOCALVERSIONBR2_VERSION_FULL这三个变量有关。 其中,BR2_VERSION虽然在Makefile里直接赋值,但仍可通过命令行变量来覆盖,即支持定制化; BR2_LOCALVERSIONsupport/scripts/setlocalversion脚本生成,查看脚本内容可知取的是VCS的版本号, 将Buildroot项目根目录下的.svn.git目录删除即可使其失效;BR2_VERSION_FULL则取前两者之一。 并且,版本号信息会写入一个文件,默认是/usr/lib/os-release及其软链接/etc/os-release。 所以,最终的版本号生成逻辑如下:

PKG_VERSION ?= 2023.02
MAKE_ARGS := $(if ${__VER__},BR2_VERSION=${PKG_VERSION}-${__VER__})
SRC_ROOT_DIR ?= /path/to/buildroot

all:
    ${MAKE} -C ${SRC_ROOT_DIR} ${MAKE_ARGS}

添加成功的版本号将会写入根文件系统,可打开相应的文件来查看,类似这样:

$ cat /etc/os-release 
NAME=Buildroot
VERSION=2023.02-194f88039230
ID=buildroot
VERSION_ID=2023.02-194f88039230
PRETTY_NAME="Buildroot 2023.02-194f88039230"

此外,Buildroot由于涉及众多项目的集成,所以还会涉及保存BusyBox编译配置、添加自定义目录及文件、 制作BusyBox显示中文的补丁文件等需求,后面完整版的Makefile将会给出针对这些内容的逻辑, 至于原理则可参考《嵌入式根文件系统构建实录》 和《Buildroot及BusyBox深度排雷》这两篇文章。

最后,给出完整内容的链接如下: