版本号管理:语义化版本号规范、版本号在Rootfs中的实践、版本号存储位置

版本号这东西,看着简单,但搞不好就是个大坑。我见过太多团队因为版本号混乱,导致升级失败、回滚困难,甚至线上事故。说白了,版本号就是Rootfs的身份证,你得让系统、让运维、让自动化工具一眼就能看懂这个文件系统是什么版本、能不能升级、会不会兼容。

这一章,我们就来聊聊版本号怎么管、怎么用、怎么存。

语义化版本号规范

先说说业界通用的语义化版本号规范(SemVer)。它的格式很简单:主版本号.次版本号.修订号,比如 2.1.3。

但每个数字背后的含义,你得搞清楚:

字段 含义 什么时候增加
主版本号 不兼容的API或重大变更 Rootfs结构、核心库、ABI发生破坏性变化时
次版本号 向下兼容的功能新增 增加新功能、新驱动、新服务,但不影响旧版本
修订号 向下兼容的问题修复 修bug、安全补丁、配置优化

核心原则:版本号一旦发布,就不能再修改。任何变更都必须通过增加新版本号来表达。

我个人习惯在Rootfs版本号后面再加一个构建元数据字段,比如 2.1.3+build20240501。这个构建号不参与版本比较,但能帮你快速定位是哪个CI流水线、哪次编译出来的。

版本号在Rootfs中的实践

理论说完了,咱们看看实际怎么用。我在项目中遇到过一个问题:两个设备,一个跑着 1.2.0,一个跑着 1.3.0,但它们的Rootfs结构完全不一样。结果升级脚本没做版本判断,直接把 1.2.0 的配置覆盖到了 1.3.0 上,导致服务全崩。

所以,版本号在Rootfs升级中,至少要干这几件事:

  • 升级路径判断:从 1.x.x 到 2.x.x 需要全量升级,从 1.2.x 到 1.3.x 可以增量升级。
  • 兼容性检查:升级脚本根据版本号决定是否执行数据迁移、配置转换。
  • 回滚决策:如果新版本启动失败,系统根据版本号判断能否回滚到旧版本。
  • 日志追溯:出问题时,版本号是定位问题的第一线索。

这里我建议你定义一个版本比较函数,而不是简单做字符串比较。因为 2.1.0 比 2.0.9 大,但字符串比较会认为 2.0.9 更大。嗯,这个坑我踩过。

// 伪代码:版本比较函数
int compare_version(const char *v1, const char *v2) {
    int major1, minor1, patch1;
    int major2, minor2, patch2;
    
    sscanf(v1, "%d.%d.%d", &major1, &minor1, &patch1);
    sscanf(v2, "%d.%d.%d", &major2, &minor2, &patch2);
    
    if (major1 != major2) return major1 - major2;
    if (minor1 != minor2) return minor1 - minor2;
    return patch1 - patch2;
}

版本号存储位置:/etc/version

版本号放哪儿?我见过有人放在 /etc/os-release、/etc/issue、甚至放在 /proc 下的虚拟文件里。但我的建议是:统一放在 /etc/version。

为什么是 /etc/version?

  • 约定俗成:很多嵌入式Linux发行版都这么干,工具链和脚本可以直接读取。
  • 简单明了:一个文件,一行内容,不需要解析复杂的INI或JSON格式。
  • 原子更新:升级时先写临时文件,再rename覆盖,保证版本号不会读到一半被截断。

文件内容格式,我建议这样:

ROOTFS_VERSION=2.1.3
BUILD_DATE=2024-05-01
BUILD_ID=ci-pipeline-1234
PRODUCT=my-embedded-device

小技巧:在构建Rootfs时,把版本号写死到 /etc/version 里。不要在启动时动态生成,否则回滚后版本号会乱掉。

我曾经遇到一个案例:某团队把版本号放在 /data 分区,结果用户重置数据后版本号丢了,系统再也无法升级。所以,/etc/version 必须放在只读的Rootfs分区里,或者至少放在不会被用户随意擦除的位置。

版本号管理流程图

下面这张图,展示了从构建到升级再到回滚的完整版本号流转过程:

Rootfs版本号管理流程图 Rootfs版本号管理流程 构建系统 生成版本号 写入 /etc/version 升级脚本读取版本号 兼容性检查 执行升级 回滚 说明:版本号在构建时生成,写入 /etc/version;升级时读取并做兼容性检查,决定升级或回滚

避坑指南

最后,分享几个我踩过的坑:

  • 版本号不要用时间戳:时间戳无法表达兼容性信息,你没法判断 20240501 和 20240502 之间有没有破坏性变更。
  • 不要依赖文件名:有人把版本号写在Rootfs镜像文件名里,但解压后文件名就丢了。版本号必须写在文件系统内部。
  • 版本号要可读:别用 v2.1.3-alpha.1+build.20240501 这种花哨格式,简单 2.1.3 加构建信息就够了。

警告:千万不要在升级过程中修改 /etc/version 的权限或属主。我曾经见过一个案例,升级脚本把 /etc/version 改成了 root:root,结果后续的OTA客户端因为权限不足读不到版本号,直接罢工了。

版本号管理,说白了就是给Rootfs一个清晰的身份标识。你想想看,如果没有版本号,你怎么知道这个设备能不能升级?怎么知道升级后能不能回滚?怎么知道出问题时该找哪个版本的代码?

所以,从今天开始,给你的Rootfs一个规范的版本号,放在 /etc/version 里,让一切都变得可追溯、可管理。