从一个前端角度来说,搞 RPM 打包这事可真够冷门的。但公司服务器有外网权限管控,每次装 pm2 都要单独申请,不如做成 rpm 丢到私有 yum 源一劳永逸。
一、背景
公司在服务器外网权限上卡得很严,访问外网需要走审批工单。但 pm2 是 Node.js 应用部署的事实标准,总不能每台机器都让运维去跑一遍 npm install -g pm2——先不说外网权限的问题,光是版本一致性就够头疼的。
干脆把它打包成 rpm,推到私有 yum 源上配成基线。新机器装系统时自动带上,运维也不用每次手动装。
二、环境准备
yum install rpm-build -y
useradd builduser
su builduser
mkdir -pv ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}
五个目录各司其职:
| 目录 | 用途 |
|---|---|
| SOURCES | 源码包就放这,后面 spec 的 Source0 从这里读 |
| SPECS | spec 文件,rpmbuild 只认这个入口 |
| BUILD | 解压和编译的工作区 |
| BUILDROOT | %install 阶段的临时根目录,不是系统路径 |
| RPMS | 打完的 rpm 最终输出到这 |
不要用 root 做 rpmbuild。 用 root 打出来的文件所有者是 root,安装后普通用户读写权限可能出问题。
三、源码准备:让 tgz 包含整个 node_modules
npm 官方的 tgz 是不带 node_modules 的——它假设你安装时会自己去解析依赖。rpm 打包显然不能这样,不能指望构建时还能联网跑 npm install。
用 npm-bundle:
npm install -g pm2
cd /usr/local/lib/node_modules/pm2
npm-bundle
mv pm2-3.2.2.tgz ~/rpmbuild/SOURCES/pm2-3.2.2.tgz
npm-bundle 把当前目录下包括 node_modules 在内的所有文件打成 tgz,丢到 SOURCES 目录即可。
四、spec 文件编写:六个坑的排查
4.1 头部定义
Name: pm2
Version: 3.2.2
Release: 1%{?dist}
Summary: pm2 program
License: AGPL-3.0
URL: https://pm2.io/
Source0: %{name}-%{version}.tgz
Requires(post): nodejs
Requires(preun): nodejs
Source0 路径要跟 SOURCES 目录的实际文件名对上。用 %{name} 和 %{version} 替代硬编码,以后升级只改 Version 一行。
4.2 %prep:解压目录名对不上
%prep
%autosetup -n package
坑一:直接把 tgz 丢进去,rpmbuild 报「找不到源代码目录」。
原因是 npm 打出来的包解压后目录名是 package,不是 pm2-3.2.2。rpmbuild 默认按 %{name}-%{version} 去找,找不到就炸。-n package 告诉它「目录叫 package」。
4.3 %install:忘写 buildroot,文件没进包
%install
mkdir -p %{buildroot}/usr/local/nodejs/lib/node_modules/pm2
cp -af . %{buildroot}/usr/local/nodejs/lib/node_modules/pm2
坑二:一开始觉得直接把文件拷过去就行,没把 %{buildroot} 当回事。
%{buildroot} 是 rpmbuild 的临时根目录,大概路径是 ~/rpmbuild/BUILDROOT/pm2-3.2.2-1.el7.centos.x86_64/。文件必须拷到这个目录下对应的子路径,之后 rpmbuild 会把它打包起来。安装 rpm 时,文件会被解到系统的对应位置 /usr/local/nodejs/lib/node_modules/pm2。
如果直接 cp 到系统路径而不是 %{buildroot} 下,rpm 包里根本没有文件。装完后系统什么都没多——排查了好一会儿才明白。
4.4 %files:.* 匹配到 . 和 ..
%files
/usr/local/nodejs/lib/node_modules/pm2/*
/usr/local/nodejs/lib/node_modules/pm2/.[a-z0-9_]*
坑三:最开始写的是 /usr/local/nodejs/lib/node_modules/pm2/.*,rpmbuild 直接报错——. 和 .. 不是文件,无法打包。换成 .[a-z0-9_]* 就能匹配所有正常的隐藏文件(.npmignore、.babelrc 等),完美避开两个特殊目录。
4.5 %preun:升级时不该删软链
%post
ln -s /usr/local/nodejs/lib/node_modules/pm2/bin/pm2 /usr/local/bin/pm2
ln -s /usr/local/nodejs/lib/node_modules/pm2/bin/pm2-dev /usr/local/bin/pm2-dev
%preun
if [ $1 = 0 ]; then
rm /usr/local/bin/pm2 /usr/local/bin/pm2-dev
fi
%post 在安装完成后把 pm2 的二进制软链到 /usr/local/bin。%preun 在卸载前执行。
坑四:$1 的语义。一开始想「卸载时删掉软链就完了」,没管这个参数:
$1 = 0:这是真正的卸载,放心删$1 = 1:这是升级过程中的旧版本卸载——新版本的%post会在后面重建软链,这里别多手
五、自动依赖误识别
坑五:在测试机上装完 rpm 跑 pm2,报错「缺少 /sbin/openrc-run」。
pm2 根本不依赖 openrc-run。排查后发现是 rpm 的自动依赖检测扫描了文件中的 shebang——pm2 代码里有段初始化模板写了 #!/sbin/openrc-run,rpm 就把这当成了硬依赖。
CentOS 7+ 用 __requires_exclude 过滤:
%global __requires_exclude ^/sbin/openrc-run$
但在 CentOS 6 上发现 %global 和 %define 都不认这个宏。最后只能暴力关掉自动检测:
AutoReq: no
副作用是必须在 Requires: 里手动声明所有依赖,漏一个就会在运行时才暴露。
六、跨系统兼容
事情到这只做了一半。项目组的构建机是 CentOS 7,但有两台遗留的 CentOS 6 机器也需要部署。换到 CentOS 6 打包后,%autosetup 报不支持,__requires_exclude 也不认。
用条件宏区分:
%if 0%{?rhel} && 0%{?rhel} >= 7
%global __requires_exclude ^/sbin/openrc-run$
%autosetup -n package
%else
%setup -n package
AutoReq: no
%endif
%setup -n package 和 %autosetup -n package 在功能上等价,只是少了个自动打补丁的能力,对纯 JS 包没影响。
另外,pm2 没有原生模块,声明 BuildArch: noarch 让打出来的 rpm 不绑定 CPU 架构,x86_64 和 arm64 都能装。
七、调试
# 构建并保留日志
rpmbuild -ba pm2.spec 2>&1 | tee build.log
# 安装测试(-ihvv 打印每步细节)
sudo rpm -ihv ~/rpmbuild/RPMS/noarch/pm2-3.2.2-1.el7.centos.noarch.rpm
# 查看安装位置和依赖
rpm -ql pm2
rpm -qpR pm2-3.2.2-1.el7.centos.noarch.rpm
# 开发阶段的静默卸载(跳过 scripts)
sudo rpm -e --noscripts pm2
构建日志里重点看两个地方:
%install阶段:确认cp后%{buildroot}下的文件结构正确——可以在 spec 的%install里临时加一行ls -la %{buildroot}/.../验证%files段:如果看到Checking for unpackaged file(s),说明有文件拷贝到了%{buildroot}但没在%files中声明
本质上就是在做一件事:把 npm install -g 这条命令的执行过程拆解成 rpmbuild 的六个阶段,保证构建结果可重复、可分发、可追溯。