npm包对应的rpm软件包制作
15 December 2018

rpm软件包制作

从一个前端角度来说,搞这个事可真够冷门的。。。但是确实遇到了需求去做了就记录一下。

目的:稍微有点规模的公司对自己的服务器都有运维规范,譬如外网权限,一般是需要申请才能访问的。然而像pm2这样通用的工具,在我司是node应用进程管理标准采用的工具,如果都要每个开发者去申请外网权限来npm install实在是麻烦,不如做成rpm包放到内部私有yum源上,也方便运维配成node应用的标准基线,无需大家手动安装。(要是npm私有源也能这么玩被我司支持能默认安装其上的包我就不用这么折腾了。。。)=.=

所以文章会介绍rpm包的制作流程,然后已pm2包的制作为示例,演示如何把npm包搞成rpm包。需要有基础的linux系统知识。

rpm包是什么,pm2又是什么

不专业解释,rpm包是linux上通用软件安装包的一种格式。pm2是node进程管理的一种命令行工具,命令行工具其实是可执行命令,对应着背后的可执行文件,可执行文件可能是多种语言的譬如bash,他们最终都会翻译成机器码由操作系统去执行(不继续了,继续我也说不下去了…)。

rpm包制作

官网文档教程最清晰:https://fedoraproject.org/wiki/How_to_create_an_RPM_package/zh-cn

制作步骤

  1. 安装rpmbuild: yum install rpm-build

  2. 切换到一个普通用户身份,不要用root身份制作rpm,不然读写文件权限都是root,用户安装包时会存在很多权限问题。

     useradd simpleuser
     su simpleuser
    
  3. 创建需要的工作目录mkdir -pv ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

  4. 将源代码包放置到~/rpmbuild/SOURCES
  5. 将写好的spec文件放置到~/rpmbuild/SPECS
  6. 打包:rpmbuild -ba NAME.spec

rpm包包含源码包SRPM 和 应用包RPM , -ba是同时构建这2种类型的包。只构建RPM包-bb,只构建SRPM包-bs

build过程

build的过程是根据spec的文件描述来进行的。

我仅简单官网解释下这个过程。build分为6个阶段,每个阶段操作内容都在之前创建的那些目录BUILD,RPMS,SOURCES,SPECS,SRPMS,每个阶段都在一个特定的文件夹下进行,都将之前阶段生成的内容先copy到当前阶段对应的文件夹下,再进行其他操作。

spec文件里的每个步骤都有一些内检指令和宏变量可用。如果这个步骤不需要特殊的操作,指令空着就好。

%prep

  • 读取的目录:%_sourcedir

  • 写入的目录:%_builddir

  • 默认动作:读取位于 %_sourcedir 目录的源代码和 patch 。之后,解压源代码至 %_builddir 的子目录并应用所有 patch。

这一步其实主要做解压的操作。可以用%autosetup,有一些参数如,-n指定SOURCE软件包解压后的名称,如果与软件名也就是.spec文件中定义的name不一致,是必须指定的,譬如pm2的tgz包解压后的文件夹名称就是package,这里就要指定%autosetup -n package

centos7以前是不支持这个指令的,只能用%setup,但是它就没办法指定-n别名了。

%build

  • 读取的目录:%_builddir

  • 写入的目录:%_builddir

  • 默认动作:编译位于 %_builddir 构建目录下的文件。通过执行类似 “./configure && make” 的命令实现。

这是个构建的步骤,如果你对源代码要执行一系列构建步骤,可以在.makefile里指定,然后在这一步执行make指令构建。

%install

  • 读取的目录:%_builddir

  • 写入的目录:%_buildrootdir

  • 默认动作:读取位于 %_builddir 构建目录下的文件并将其安装至 %_buildrootdir 目录。这些文件就是用户安装 RPM 后,最终得到的文件。

这里要注意的是,你希望最终你的安装包文件被存放到哪里,在这步里就要把它先copy到%{buildroot}下的该目录下。一般软件包的安装位置都是有规范的,默认不做这步就直接安装到了根目录下了,这样肯定太好。

%check

  • 读取的目录:%_builddir

  • 写入的目录:%_builddir

  • 默认动作:检查软件是否正常运行。通过执行类似 “make test” 的命令实现。很多软件包都不需要此步。

bin

  • 读取的目录:%_buildrootdir

  • 写入的目录:%_rpmdir

  • 默认动作:读取位于 %_buildrootdir 最终安装目录下的文件,以便最终在 %_rpmdir 目录下创建 RPM 包。在该目录下,不同架构的 RPM 包会分别保存至不同子目录, “noarch” 目录保存适用于所有架构的 RPM 包。这些 RPM 文件就是用户最终安装的 RPM 包。

这步是内置的步骤,不对开发者透出

src

  • 读取的目录:%_sourcedir

  • 写入的目录:%_srcrpmdir

  • 默认动作:创建源码 RPM 包(简称 SRPM,以.src.rpm 作为后缀名),并保存至 %_srcrpmdir 目录。SRPM 包通常用于审核和升级软件包。

源码RPM包也不是必须的。

可以关注下build时的日志来更好的了解这个过程:

spec文件

也是只记几个要注意的点:

  • Name: 软件包名,应与 SPEC 文件名一致。命名必须符合 软件包命名规定。
  • Version: 上游版本号。请查看 版本标签规定。如果包含非数字字符,您可能需要将它们包含在 Release 标签中。如果上游采用日期作为版本号,请考虑以:yy.mm[dd] (例如 2008-05-01 可变为 8.05) 格式作为版本号。
  • Release: 发行编号。初始值为 1%{?dist}。每次制作新包时,请递增该数字。当上游发布新版本时,请修改 Version 标签并重置 Release 的数字为 1。具体参考打包规定中的 Release 标签部分,以及 Dist tag。
  • Summary: 一行简短的软件包介绍。
  • Group
  • License
  • URL: 该软件包的项目主页。
  • Source0: 软件源码包的 URL 地址。不一定是url,也可以是SOURCE文件夹下的源码压缩包名。”Source” 与 “Source0” 相同。强烈建议提供完整 URL 地址,文件名用于查找 SOURCES 目录。如果可能,建议使用 %{name} 和 %{version} 替换 URL 中的名称/版本,这样更新时就会自动对应。下载源码包时,需要 保留时间戳。如果有多个源码包,请用 Source1,Source2 等依次列出。如果你需要添加额外文件,请将它们列在后面。
  • Patch0
  • BuildArch
  • BuildRoot
  • BuildRequires: 编译软件包所需的依赖包列表,以逗号分隔。此标签可以多次指定。编译依赖 不会 自动判断,所以需要列出编译所需的所有依赖包。常见的软件包可省略,例如 gcc。如果有必要,你可以指定需要的最低版本(例:”ocaml >= 3.08”)。如果你需要找到包含 /EGGS 文件的软件包,可执行 “rpm -qf /EGGS”。如果你需要找到包含 EGGS 程序的软件包,可执行 “rpm -qf which EGGS”。请保持最小依赖(例如,如果你不需要 perl 的功能,可使用 sed 代替),但请注意,如果不包含相关依赖,某些程序会禁用一些功能;此时,你需要添加这些依赖。
  • Requires: 安装软件包时所需的依赖包列表,以逗号分隔。请注意, BuildRequires 标签是编译所需的依赖,而 Requires 标签是安装/运行程序所需的依赖。大多数情况下,rpmbuild 会自动探测依赖,所以可能不需要 Requires 标签。然而,你也可以明确标明需要哪些软件包,或由于未自动探测所需依赖而需要手动标明。
  • %description
  • %prep: 打包准备阶段执行一些命令(如,解压源码包,打补丁等),以便开始编译。一般仅包含 “%autosetup”;如果源码包需要解压并切换至 NAME 目录,则输入 “%autosetup -n NAME”。
  • %build: 包含构建阶段执行的命令,构建完成后便开始后续安装。程序应该包含有如何编译的介绍。
  • %install: 包含安装阶段执行的命令。命令将文件从 %{_builddir} 目录安装至 %{buildroot} 目录。
  • %check: 包含测试阶段执行的命令。
  • %clean: 清理安装目录的命令。一般只包含:rm -rf %{buildroot}
  • %files: 需要被打包/安装的文件列表。
  • %changelog: RPM 包变更日志。不是软件本身的变更日志。

%files

比较需要注意的是%files

此部分列出了需要被打包的文件和目录。所以需要最终在用户安装后系统里出现的文件都必须在这里定义。%{buildroot}文件夹下的最终文件都要在这里列出。可以使用glob通配符。

如果存在以下情况,可能引发错误:

  • 未匹配到任何文件或目录。也就意味着你的rpm安装完后,用户系统里没有任何文件变更,那你的软件包功能是?
  • 文件或目录被多次列出。避免重复
  • 未列出 %{buildroot} 下的某个文件或目录

您也可以使用 %exclude 来排除文件。这对于使用通配符来列出全部文件时会很有用,注意如果未匹配到任何文件也会造成失败。

Scriptlets

当用户安装 RPM 时,您可能想要执行一些命令。这可以通过 scriptlets 完成。

脚本片段可以:

  • 在软体包安装之前 (%pre) 或之后 (%post) 执行
  • 在软体包卸载之前 (%preun) 或之后 (%postun) 执行
  • 在事务开始 (%pretrans) 或结束 (%posttrans) 时执行

譬如我一开始做pm2的rpm包方式就是将pm2源码的tgz包,在%post步骤里npm install -g pm2-2.3.2.tgz一把来实现。后来还借助npm来install有点多此一举。

  • %{_bindir}: /usr/bin
  • %{_builddir}: ~/rpmbuild/BUILD
  • %{buildroot}: ~/rpmbuild/BUILDROOT

测试

制作好了后当然需要自己先安装测试下了

安装:sudo rpm -ihvv ~/rpmbuild/RPMS/${name}.rpm

你构建的软件包究竟叫什么可以去~/rpmbuild/RPMS下查看,譬如我制作的pm2就在~/rpmbuild/RPMS/x86_64/pm2-3.2.2-1.el7.centos.x86_64.rpm

-ihv参数就能安装,-ihvv会打印安装过程中每步的信息,方便debug。

卸载rpm包:sudo rpm -e ${name},万一你写了Scriptlets并且跟我一样悲催的在初始开发时写的%preun脚本有问题,那么执行报错会导致卸载失败,可以通过sudo rpm -e --noscripts ${name}来不执行脚本单纯卸载

更多查找rpm包相关信息的命令

查找所有安装过的包含某个字符串的软件包: rpm -qa | grep ${string_name}

查看rpm包中的文件安装位置:rpm -ql ${name}

查看某rpm包中包含文件列表:rpm -ql ${name}.rpm

查看某可执行命令是哪个软件包安装的:

rpm -qf `which 命令名`    #返回软件包的全名
rpm -qif `which 命令名`   #返回软件包的有关信息
rpm -qlf `which 命令名`   #返回软件包的文件列表

查看某个文件是哪个软件包安装的:

whereis ${filename}
rpm -qf ${filepath}

npm包转成rpm包

npm install 干了什么

  1. 从远程下载压缩包tgz
  2. 解压缩到规范的指定目录,譬如,npm安装在/usr/local/nodejs/lib/node_modules
  3. 安装该包的依赖包
  4. 如果有可执行命令,创建可执行命令所在文件(package.json里的bin所指向的文件)的软连接到可被系统查找到的命令目录,一般npm包的都是/usr/local/nodejs/bin

清楚了这个也就能清楚制作rpm包时需要做什么了。

获取npm包源码

直接从npm源找到的tgz包是不包含该包的依赖包,npm pack在原包安装目录下打的也是一样。一般做rpm包我们是把完整的源码整个拉下来,免得build的过程中再去拉取。

打出包含node_modules文件夹的安装包可以借助npm-bundle这个npm包工具。

pm2来举例:

  • 安装pm2:npm install -g pm2
  • 进入包源码目录:cd /usr/local/lib/node_modules/pm2
  • 执行npm-bundle打出包含node_modules目录依赖的tgz源码压缩包。然后放到上面说的rpmbuild/SOURCE,供后续rpmbuild使用

pm2 的rpm包spec文件示例

centos7版:

Name:           pm2
Version:        3.2.2
Release:        1%{?dist}
Summary:        pm2 program
Summary(zh_CN): pm2 程序

License:        AGPL-3.0
URL:            https://pm2.io/doc/en/runtime/overview/
Source0:        %{name}-%{version}.tgz

Requires(post): nodejs
Requires(preun): nodejs

%global __requires_exclude ^/sbin/openrc-run$

%description
node pm2 for rpm version

%prep
%autosetup -n package

%build


%install
mkdir -p %{buildroot}/usr/local/nodejs/lib/node_modules/pm2
cp -af . %{buildroot}/usr/local/nodejs/lib/node_modules/pm2

%clean
rm -rf %{buildroot}

%files
/usr/local/nodejs/lib/node_modules/pm2/*
/usr/local/nodejs/lib/node_modules/pm2/.[a-z0-9_]*

%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
ln -s /usr/local/nodejs/lib/node_modules/pm2/bin/pm2-docker /usr/local/bin/pm2-docker
ln -s /usr/local/nodejs/lib/node_modules/pm2/bin/pm2-runtime /usr/local/bin/pm2-runtime

%preun
if [ $1 = 0 ] ; then
rm /usr/local/bin/pm2 /usr/local/bin/pm2-dev /usr/local/bin/pm2-docker /usr/local/bin/pm2-runtime
fi

%changelog

%post中做了软连接的操作,软链目标路径只要是$PATH中存在的路径就好,npm install时虽然是用了/usr/local/nodejs/bin,但是这个路径不一定在$PATH中,为了简化处理,我直接软链到了更通用的/usr/local/bin,当然用更更通用的/usr/bin更保险

遇到的一些问题

指定安装目录

要在%install里创建目标安装目录,将源码文件移到这个目录里。

安装时报...文件或文件夹找不到

之前%files定义的是:

/usr/local/nodejs/lib/node_modules/pm2/*
/usr/local/nodejs/lib/node_modules/pm2/.*

这样会把/usr/local/nodejs/lib/node_modules/pm2/./usr/local/nodejs/lib/node_modules/pm2/..包含进去,且不能通过%exclude指令排除掉,后改成了:

%files
/usr/local/nodejs/lib/node_modules/pm2/*
/usr/local/nodejs/lib/node_modules/pm2/.[a-z0-9_]*

安装时报目标机上/sbin/openrc-run依赖找不到

rpm确实有依赖自动识别机制,能识别到这个依赖说明pm2里用了它,一搜发现有个模板文件.tpl中含有#!/sbin/openrc-run,所有这种指令都会被自动依赖识别,譬如#!/bin/bash会被识别依赖sh。但是pm2并不是真的依赖openrc-run这个系统初始化工具啊,他是会判断宿主机环境,再去判断用哪一种的,譬如我的测试机上用的就是systemd而不是openrc-run,所以这并不是一个强依赖。

需要有个姿势告诉rpm的自动依赖识别系统过滤它。方案就是centos7上是有__requires_exclude__requires_exclude_from的用法的。详情见https://fedoraproject.org/wiki/Packaging:AutoProvidesAndRequiresFiltering

兼容问题

制作rpm包还要注意打包机和安装机的系统版本差异,不同版本对rpm支持度不一样。

在centos6中%global __requires_exclude宏不支持,无奈,部分文档中提的%define __requires_exclude我试验了也不支持,我只能通过AutoReq: no暴力关掉所有自动依赖分析。

centos6中也不支持%autosetup,不能解决rpm包和解压后文件目录名不一致问题,只能先将npm-bundle打的压缩包解压,将package文件夹名改成pm2-3.2.2,再通过tar -zcvf pm2-3.2.2.tgz pm2-3.2.2压缩回去。

参考