上个月在整理内部服务器清单时,发现一台新入库的KeyarchOS机器上有个任务始终没跑起来——cron里每天凌晨都要执行一次calendar输出当日日程到审计日志,但日志里连续几天都是command not found。查了一圈,根子是包管理器里根本没有calendar,而运维拿来的安装包还是calendar-1.28-1.20140613cvs这种2014年风格的RPM。说白了,这就是一个典型的国产操作系统适配老软件包的问题:二进制包装不上、源码得重新编译、spec要改,还得保证行为不变。
把整个适配过程走完之后,我最大的感受是:这种老包的适配,难度不在代码本身,而在"破案"——顺着版本号、依赖关系和构建日志一步一步往回倒,搞清楚十年前这个包是怎么生产出来的,然后在今天的系统上把这个过程重演一遍。这篇文章就把我这次在KeyarchOS上适配calendar-1.28-1.20140613cvs的完整链路写出来,包括怎么分析RPM、怎么修编译错误、怎么重写spec、怎么验证功能。如果你也在国内Linux发行版上碰到类似的老RPM包,这篇的排查思路可以直接复用。
1. 为什么要在KeyarchOS上折腾一个十年前的老日历包
1.1 calendar这个工具到底是干嘛的
先说清楚对象。这里的calendar不是图形界面的日历应用,也不是安卓上的日历App,而是一个源自BSD的命令行日程工具。它的工作方式非常朴素:系统在/usr/share/calendar目录下放一批日期定义文件,比如calendar.usholiday、calendar.christian,用户也可以在自己的目录下放一个~/.calendar文件,里面按行写"月份/日期 + 日程描述"。运行calendar命令时,它会读取今天和接下来几天的日程,然后打到标准输出。
这个工具的典型用法是配cron。比如我这边实际部署的场景:
0 7 * * * /usr/bin/calendar -d | mail -s "Today Schedule" ops@internal每天早上7点把当天日程发到邮箱,服务器端不需要任何图形环境,几十KB的二进制就够了。对自动化运维和内部审计来说,这种极简工具比安装一个完整的日历套件实用得多——没有数据库、没有后台服务、不占端口,输出是纯文本,方便进日志系统。
1.2 谁还会在2025年用到这么老的包
很多人第一反应是"2014年的包还有人用?"。但现实就是,企业内部遗留系统的粘性远比想象中大。我在这次适配前梳理了一下使用场景:
- 审计留痕:某些内部流程要求每天记录排班、维护窗口等日程,calendar配合cron输出纯文本日志,格式固定,便于grep。
- 脚本依赖:个别老脚本直接调用了
/usr/bin/calendar处理日期判断,比如"判断今天是否为季度末"这类逻辑,依赖calendar的日期库。 - 迁移合规:原来的CentOS环境跑得好好的,现在服务器换成了KeyarchOS,应用层不能变,否则下游对接方要跟着改。
所以这个适配的目标很明确:不是把代码现代化,而是让一个在旧系统上正常工作、新系统上装不上的RPM包,经过重新构建后能在KeyarchOS上以同样的方式安装、运行、输出。行为必须和旧环境一致。
1.3 适配完成后的交付物是什么
我在接这个任务时给自己列了一个交付清单,不只是"能跑了"这么简单:
- 一个可在KeyarchOS上安装的RPM包:
calendar-1.28-1.20140613cvs.keyarchos.x86_64.rpm - 一套可复用的spec文件:以后如果KeyarchOS升级内核或工具链,可以用
rpmbuild -ba重新产出包。 - 一份功能验证记录:证明日历输出、日期计算、cron集成这些核心行为和旧环境一致。
这个清单帮我框定了工作量,也避免了"装上了但没测透"的情况。后面所有步骤都是在为这三项交付服务。
2. 动手之前:把RPM包的"底细"摸透
2.1 从版本号读出关键线索
拿到calendar-1.28-1.20140613cvs这个包名,第一件事不是急着装,而是拆解它的版本号命名规则。这里有个容易忽略的信息量:
1.28是上游版本号,对应calendar工具自身的版本。1.20140613cvs是RPM的release号,其中20140613是构建日期,cvs表示这份代码是从CVS版本控制系统的某个快照拉出来的。
这种"release里带日期和cvs"的命名风格,在2014年前后的Linux发行版里非常常见,尤其是Fedora边源和EPEL里一些缺乏正式发版节奏的小工具。它暗示了一件事:这个包的构建方式很可能是"从CVS checkout代码 -> 打补丁 -> 用rpmbuild打包"。也就是说,原包不一定是纯tar包,而可能是版本库里某个时间点的快照外加若干补丁。
搞清楚这个命名规则后,我开始判断这个包属于哪个上游家族。语法上"月份/日期"的行格式、默认读取/usr/share/calendar目录下的calendar.*文件、支持-t参数指定日期、-A参数指定天数——这些都是BSD calendar的经典行为,所以它的上游源码基本可以确定是BSD系或者基于BSD移植的版本。这一点在后面的源码编译环节很关键,因为BSD代码对于glibc和GCC标准库的依赖习惯和GNU项目不太一样。
2.2 用rpm命令做一次"尸体解剖"
老规矩,先把RPM包的元数据、文件清单、依赖关系全拆出来看一眼。我习惯用一条命令分别拿三组信息:
rpm -qip calendar-1.28-1.20140613cvs.x86_64.rpm rpm -qlp calendar-1.28-1.20140613cvs.x86_64.rpm rpm -qpR calendar-1.28-1.20140613cvs.x86_64.rpm执行后关键信息如下:
| 项目 | 解析结果 |
|---|---|
| Name | calendar |
| Version | 1.28 |
| Release | 1.20140613cvs |
| Architecture | x86_64 |
| 依赖 | /bin/sh,libc.so.6(GLIBC_2.14),rtld(GNU_HASH) |
| 文件列表 | /usr/bin/calendar、/usr/share/calendar/calendar.usholiday、/usr/share/man/man1/calendar.1.gz等 |
这里有两个点值得注意。
第一,它对glibc的依赖是GLIBC_2.14,这个要求很古老。理论上2025年的KeyarchOS上glibc版本至少在2.28以上,这种版本符号要求是向下兼容的,所以直接安装时一般不会卡在glibc版本上。
第二,它只依赖/bin/sh,没有任何额外的动态库依赖。这得益于calendar本来就是独立的小工具,编译时只链接了libc。这意味着如果这个二进制能被加载运行,基本不会遇到"缺库"的经典问题。
2.3 实际安装时的报错才是真正的缺口
理论上依赖很干净,那为什么装不上?直接在KeyarchOS上试一次:
rpm -ivh calendar-1.28-1.20140613cvs.x86_64.rpm报错内容大概是:
error: Failed dependencies: /bin/sh is needed by calendar-1.28-1.20140613cvs.x86_64看到这个报错我愣了一下。KeyarchOS上/bin/sh明明存在,而且指向的是bash。查了一圈,问题出在/bin在KeyarchOS上的默认文件系统布局:新版系统里/bin是usr/bin的符号链接,而老版RHEL/CentOS的rpm会在事务中单独校验/bin/sh这个文件路径是否存在,符号链接场景下某些rpm版本会判定依赖不满足。
这个坑非常典型。它不是"缺依赖",而是"打包时间太早,用的依赖写法太死"。解决办法不是强行--nodeps装进去——一旦强制装,后续rpm -e、rpm -V校验都会出问题。正确路线是重新构建一个适配KeyarchOS的RPM,在spec里把/bin/sh的依赖改成/bin/sh或直接写成bash,同时让构建环境生成新的依赖标记。
这一步也确认了我的判断:这个包必须走"源码重新构建"路线,而不是"二进制迁移"路线。
3. 依赖断点与源码构建环境准备
3.1 先把构建工具链补齐全
KeyarchOS本身装了gcc、make这些基础工具,但rpmbuild不一定在默认安装里。我这边缺失的是rpm-build包。补齐的命令:
dnf install -y rpm-build dnf groupinstall -y "Development Tools"补完后验证一下rpmbuild可用:
rpmbuild --showrc | grep -E "topdir|buildarch"拿到源码包之后,rpmbuild才真正开始起作用。这里提醒一句:如果在内网离线环境做适配,最好提前准备一份rpm-build的离线仓库,或者把构建机和配有网环境的机器放在同一个网络区域,否则临时装工具会非常痛苦。
3.2 源码树里藏着第一个编译断点
找到了配套的SRPM包(calendar-1.28-1.20140613cvs.src.rpm),在KeyarchOS上解包:
rpm -ivh calendar-1.28-1.20140613cvs.src.rpm解包后源码在~/rpmbuild/SOURCES/calendar-1.28目录下,spec文件在~/rpmbuild/SPECS/calendar.spec。
然后执行普通的构建流程:
rpmbuild -ba ~/rpmbuild/SPECS/calendar.spec第一次构建不出意外地挂了。报错关键行是:
calendar.c: In function 'getdate': calendar.c:123:15: error: implicit declaration of function 'stpcpy'这个错误在2025年看到其实一点不意外。老代码写于2014年甚至更早,当时GCC对函数隐式声明只给警告,不给错误。但GCC的默认标准在十几年间升级了好几轮,从-std=gnu89到默认-std=gnu11乃至更高,把隐式声明从警告抬高成了错误。
这里值得展开说一下原理。stpcpy这个函数在POSIX 2008标准里才被正式纳入,旧代码默认认为"调用一个未声明的函数也能编译过"。新版glibc里stpcpy的声明是有条件暴露的,通常需要_GNU_SOURCE或_POSIX_C_SOURCE >= 200809L才会在头文件里出现。老代码没有设置这些宏,于是编译器在string.h里看不到stpcpy的声明,就报隐式声明错误。
修复方案很朴素,在源码里加一个宏定义,或者编译时加-D_GNU_SOURCE。我选择了修改Makefile,在CFLAGS里追加:
CFLAGS += -D_GNU_SOURCE -O2 -g这一处改完,stpcpy的声明问题消失,但紧接着又冒出来第二个编译断点。
3.3 链接阶段的隐藏断点:bsd兼容函数
第二个报错出在链接阶段:
/tmp/ccXXXX.o: undefined reference to 'strlcpy' collect2: error: ld returned 1 exit statusstrlcpy同样是BSD系的常用函数,但glibc直到比较晚的版本才在string.h里暴露声明,而且链接时它属于libbsd的范畴,不像stpcpy那样直接从glibc的库里就能找到符号。
这个断点的本质是:calendar的上游代码默认自己运行在"类BSD"环境里,它直接使用了BSD的字符串函数。而KeyarchOS是标准的glibc/Linux环境,没有默认提供这些函数的实现。
我查了一下系统里有没有libbsd:
dnf search libbsd dnf install -y libbsd-develKeyarchOS的仓库里是有libbsd-devel的,装上之后在Makefile里把-lbsd加到LDFLAGS:
LDFLAGS += -lbsd这个做法在Linux发行版上构建BSD系工具时非常常见:不改源码里的函数调用,只用libbsd提供的兼容实现,保持上游代码尽量少动。
3.4 环境细节:locale和时区对calendar输出的影响
编译一过,就以为万事大吉是大忌。calendar这种工具,输出内容完全依赖日期和本地化规则,而构建机和运行机的locale、timezone配置稍有不同,输出就会跟着变。比如calendar内部判断节假日时,可能会读取/usr/share/calendar下带locale后缀的文件(calendar.zh_CN之类),如果构建时没有安装对应的locale数据,运行时就会静默降级成空白日程。
我这边虽然不需要中文节假日,但为了行为一致,还是在spec的%post脚本里显式设置了软链:
ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime以及在%build阶段强制指定LANG=en_US.UTF-8、LC_TIME=C,保证构建期间生成的man page和默认日历文件不因为locale不同而内容漂移。这种细节如果不在构建时锁死,后面功能回归阶段会浪费大量时间。
4. 重写spec文件与rpmbuild全过程
4.1 先理清原spec的结构
把原spec打开看了一遍,整体框架没问题,毕竟是2014年从官方渠道流出来的包,打包规范是标准的。核心段落有:
Source0: calendar-1.28.tar.gzPatch0: calendar-1.28-cvs.patch%build里执行make%install里执行make install DESTDIR=%{buildroot}
我保留了Source0和Patch0,因为这些是上游代码的正确基线。改动点集中在以下三处。
4.2 三个必须改的地方:Release、依赖写法、构建参数
第一处是Release号。为了和原包区分,避免在同一个系统里出现两个release号相同的RPM,我把Release改成了1.20140613cvs.keyarchos%{?dist}。这样产出的包是calendar-1.28-1.20140613cvs.keyarchos.x86_64.rpm,一眼就能看出是适配版本,也符合KeyarchOS对第三方适配包的命名习惯。
第二处是依赖写法。原spec里写死Requires: /bin/sh,在KeyarchOS上引发了本篇第2节说的路径依赖坑。我把这里的依赖改成:
Requires: /bin/sh Requires: bash虽然KeyarchOS的/bin/sh实际就是指向bash的符号链接,但显式加上bash依赖更稳妥,而且rpm做依赖解析时不会再纠结/bin/sh的路径类型。
第三处是构建参数。把我在3.2节和3.3节验证过的-D_GNU_SOURCE和-lbsd写进spec的%build段,确保后续任何人在这个环境里执行rpmbuild -ba,都能稳定复现成功结果:
make %{?_smp_mflags} CFLAGS="-D_GNU_SOURCE -O2 -g" LDFLAGS="-lbsd"4.3 完整的rpmbuild执行过程
spec改完后执行:
rpmbuild -ba ~/rpmbuild/SPECS/calendar.spec这一步会把整个流程重跑一遍:%prep解压源码、打补丁,%build编译,%install安装到临时root,%files列表校验,最后生成二进制RPM和SRPM。
这里有个经验:rpmbuild的输出信息里,如果%files阶段出现"File not found"错误,通常不是文件真的不存在,而是%install阶段的安装路径没对齐。我这次就遇到了/usr/share/man/man1/calendar.1.gz找不到的报错,原因是源码里的Makefile安装man page时用的目录是/usr/man而不是/usr/share/man。修复方式是在spec的%install段加一句:
mkdir -p %{buildroot}%{_mandir}/man1 install -m 0644 calendar.1 %{buildroot}%{_mandir}/man1/calendar.1这种"源码自带的安装路径旧"的问题,在2014年的包中不少见,处理思路就是不要在%files里硬凑,而是统一在%install阶段把文件放回标准的FHS路径上。
4.4 安装新包并做一次完整性校验
构建完拿到的产物:
~/rpmbuild/RPMS/x86_64/calendar-1.28-1.20140613cvs.keyarchos.x86_64.rpm安装并校验:
rpm -Uvh calendar-1.28-1.20140613cvs.keyarchos.x86_64.rpm rpm -V calendarrpm -V输出为空时,说明文件属性和校验和与打包时一致,安装是干净的。接着确认核心文件确实落在预期位置:
ls -l /usr/bin/calendar /usr/share/calendar/calendar.usholiday到这里,RPM包本身已经能在KeyarchOS上正常安装了。
5. 回归验证与想清楚老日历在现代场景的位置
5.1 功能回归用例:不能只验证"能跑"
一个命令行工具适配完成后,至少要跑一遍核心功能。calendar的价值在于日期计算和日程输出,我设计了下面这组回归用例:
| 用例 | 命令 | 预期结果 |
|---|---|---|
| 基础输出 | calendar | 输出今日及未来若干天的日程 |
| 指定日期 | calendar -t 2025/12/25 | 输出2025年12月25日的日程 |
| 扩展天数 | calendar -A 7 | 输出未来7天的日程 |
| 用户自定义 | 在~/.calendar写一行自定义日程后运行 | 用户日程出现在输出中 |
| 系统日历数据 | cat /usr/share/calendar/calendar.usholiday | 节假日定义文件完整 |
| man page | `man calendar | head -5` |
这组用例全部通过,基本可以确认适配没有破坏原有行为。特别推荐大家把"用户自定义日程"这个用例放在最后跑,因为只有它真正测到了用户态读写路径,比单纯看系统节假日文件要深一层。
5.2 同一个"日历"关键词下的另一个世界:fossify calendar
搭这套命令行日历的过程中,我顺便留意到现在搜索"calendar"热词时,经常看到fossify calendar这个项目。它是安卓端的一个开源日历应用,强调的是无广告、离线可用、隐私友好,支持事件提醒、农历视图、ICS导入导出这些功能。
我把两者放到一起看,不是为了分高下,而是想说明一个事:同样是"日历",服务端命令行工具和个人移动端应用是两条完全不同的产品线。calendar-1.28-1.20140613cvs解决的是"无人值守环境里如何自动生成、投递日程文本"的问题,它的竞品是cron、邮件和脚本;fossify calendar解决的是"个人如何在手机上管理事件、获取提醒"的问题,它的竞品是Google Calendar和系统自带日历。
理解这个边界很有实际意义。很多人在服务器上调不出图形日历就认为"日历适配没用",其实是要区分使用场景。如果哪天KeyarchOS上需要跑安卓容器、做移动端日历应用的交叉编译,那fossify calendar的开源代码反而是更好的研究对象——它那套用Kotlin写的日程存储、事件提醒模块,可以迁移到桌面端复用。
5.3 这次适配沉淀下来的通用方法
把整个流程回头看一遍,适配老RPM包的关键点可以收敛成六步排查法:
- 先拆版本号:
1.28-1.20140613cvs里的上游版本、release、日期、版本系统代号都要拆出来,判断它的上游代码来自哪里、有多老。 - 再拆二进制包:
rpm -qip、rpm -qlp、rpm -qpR三连,把依赖关系、文件清单、架构信息全部拿到手。 - 直接安装做验证:用一次真实的
rpm -ivh试错,拿到具体的依赖报错,判断是路径问题、glibc问题还是缺包。 - 从SRPM重构建:能拿到源码包就优先走源码重打包路径,不要用
--nodeps硬装二进制包。 - 逐个击破编译错误:隐式声明、缺库、安装路径这类问题,每一个都要搞清楚背后的标准差异,再用补丁或spec参数修复。
- 回归验证与打包命名:验证功能完整后,一定要做release重命名、依赖改写、完整性校验,确保成品符合新系统的安装规范和命名预期。
这次适配calendar-1.28-1.20140613cvs,本身不是一个高深项目,但恰恰是这样"不起眼"的老包,最能考验一个工程师对RPM生态、编译工具链演进和系统兼容性的理解深度。我在操作过程中一个比较深的体会是:不要急着改代码,先把"这个包十年前是怎么构建出来的"这个问题回答清楚,后面每一步都能少走弯路。