基于官方 rpm 快速创建自定义 rpm

一、扯淡

由于工作中常常需要使用 yum 安装一些软件,而当需求特定版本(一般是比较新的版本)时,往往 CentOS 官方提供的都不是最新的,这时候一般解决方案是先 rpm 安装,然后再用高版本的 二进制文件进行替换,当机器多的时候这种方法很蛋疼,所以决定研究了一下 rpm 制作方法,以下为一些实践。

二、spec 简介

关于 spec 文件的具体作用和详细的介绍网上已经有很多文章,具体可参考 RPM 打包技术与典型 SPEC 文件分析,本文主要主要以概述的形式来大致介绍以下 spec 文件

2.1、spec 文件作用

在从0开始创建 rpm 时,使用 rpm build 工具基于一个预定义好的 spec 文件来创建 rpm;也就是说 spec 文件里定义了 rpm 如何创建以及创建过程,比如 rpm 内包含的资源文件、配置信息、安装卸载前后执行脚本、rpm 说明信息等;如果能完全了解了 sepc 文件,那么理论上就可以自己从 0 开始创建一个 rpm 包,然而对于书写 spec 就像写代码一样,有些时候自己写的也能跑起来…但是考虑的地方对比官方的总有些不足,所以如果可以最好的做法是基于官方 spec 修改,CentOS 官方的 rpm spec 可在 CentOS 官方仓库 中获取

2.2、spec 文件结构

参考 RPM 打包技术与典型 SPEC 文件分析 这篇文章可知,spec 文件其实就是个模板文件,里面包含各种宏定义,其大之分为六大段:

2.2.1、 文件头

文件头部分主要是对即将编译出的 rpm 包做一些声明,包括但不限于 rpm 名称、编译平台、适用平台、版权信息、资源文件位置、说明信息 等等

2.2.2、 %prep段

prep 顾名思义意为预处理段,预处理段主要是一些预处理脚本,比如 安装前执行哪些动作(创建用户什么的)、安装后执行那些动作,还有卸载前卸载后等

2.2.3、 %build段

对于需要编译的软件,比如 nginx,可在此段写好编译命令,然后在打包 rpm 的时候就可以直接编译成二进制文件被打入 rpm 包

2.2.4、 %install段

一般编译阶段就会执行 install 命令,但这个时候 install 的并非系统目录,而是指定一个临时目录,把他当成系统的根目录进行安装,此时编译好的二进制安装文件全部在这个临时目录下;install 段的作用就是定义临时目录这些文件将来在安装 rpm 包的时候究竟释放到系统的那些位置,包括权限是什么等等

2.2.5、 %files段

在 install 段定义好临时目录下的文件的释放位置后,为防止无意外发生,还需要在 files 段中重新定义一些到底释放了那些文件,文件类型是什么;比如某某文件时配置文件,某某文件是可执行文件等,files 段相当于一个文件清单,且必须和 install 段定义的文件相匹配,也就是说你 install 了10个文件,那么 files 段必须声明这 10 个文件,当然可以使用通配符,但不能有缺失,否则 rpm 编译不过

2.2.6、 %changelog段

顾名思义,这段主要记录 spec 文件的修改历史和修改原因

三、创建自定义 rpm

3.1、基于官方 spec 的不可行分析

对于创建一个自定义的 rpm 一半有两种可选方案,一种是从 0 开始,自己参考官方的 spec 文件自己写一个 spec,然后自己编译;但是根据个人实践经验来看,这招很坑爹;主要有以下几个原因导致不可行:

  • 文件缺失: 官方 spec 文件放在 git 仓库中,其中有些本地的资源文件全部加入到 git 的忽略文件中,那些奇奇怪怪名字的文件你也不知道是啥玩意,所以没法准备环境
  • 理解困难: 一般官方的 spec 都会非常吊…吊到你根本看不懂,小说上百行多的上千行,里面各种复杂的环境配置和宏替换,对于非运维专业的童鞋来说,甚至你想改个编译版本都极其困难
  • 网络环境: 官方的 spec 一般都是从源码开始重新编译,而源码常常是直接从 github 拉取,众所周知的原因 github 存放在亚马逊 S3 上的文件在国内下载是以 b/s 的速度计算的,一个G的源码能 wget 1年
  • 性能不足: 官方 sepc 不管什么玩意都是从源码编译的,比如 kubernetes,这东西官方有提供编译后的可执行二进制文件,而且跨平台;根本无须自己重新编译;我测试过在一台国外的 16G 8核心的 vps 上编译一次 kubernetes 需要半小时时间
  • spec 无缓存: 使用 spec 文件编译 rpm 不像 docker build image,rpm 编译是没有缓存的,也就是说你写了 100 行的 spec 文件,你的第 100 行写错了,那么你修改以后前面 99 行还是得重来一遍,试想一下编译 kubernetes 的 rpm 时用自己写的 spec 是件多么恐怖的事情…轻轻松松一上午时间没了…对…没了,然后还啥也没干…

3.2、基于已有 rpm 的实践

综上所述,最好的做法就是基于已有的 rpm 进行制作,比如说官方提供了 kubernetes 1.2 版本的 rpm,我们需要 1.3.6 的,那么就可以下载官 1.2 的 rpm 然后替换一些 1.3.6 的可执行文件再打包成新的 rpm 即可,这个过程涉及到几个问题:

  • 如何下载官方的 rpm : 借助 yumdownloader 工具可直接通过 yum 下载
  • 如何解包 rpm : 使用 rpm2cpio package.rpm | cpio -idmv 可接包
  • rpm prep 段怎么处理: 可以使用 rpm -qp --scripts package.rpm 从 rpm 包中提取
  • 没有 spec 如何封包: 使用 fpm 工具即可

3.3、撸一个 etcd 的 rpm

以下以创建一个 etcd 的 rpm 为例,创建基于官方提供的 rpm 并替换掉可执行文件的方式;其实对于官方的 rpm 我们实际只关心两样东西: 第一个就是里面的文件,第二个就是 prep 段的脚本,以下是实际的制作过程

3.3.1、下载官方 rpm

1
2
3
4
# 先安装 yum 工具
yum install -y yum-utils
# 使用 yumdownloader 下载
yumdownloader etcd

3.3.2、提取 prep 段

一般官方的 rpm 主要包含 4 个部分的 prep 脚本,分别为: 安装前处理(preinstall)、安装后处理(postinstall)、卸载前处理(preuninstall)、卸载后处理(postuninstall);我们可以将其提取后放到 4 个文件中,在使用 fpm 重新封包时指定一下即可

1
2
# 首先看一下官方 rpm 的预处理脚本
rpm -qp --scripts etcd-2.3.7-2.el7.x86_64.rpm

显示的脚本信息如下

hexo_makerpm_prep

将这四段脚本分别保存为四个文件即可,比如保存为 preinstall.sh、postinstall.sh…

3.3.3、解包 rpm

首先创建一个临时目录,用于存放解包后的 rpm 文件,这个目录相当于 rpm 安装后的系统根目录,然后将 rpm 解包即可

1
2
3
4
5
# 创建临时目录
mkdir rpmfiles
# 解包
cp etcd-2.3.7-2.el7.x86_64.rpm rpmfiles && cd rpmfiles
rpm2cpio etcd-2.3.7-2.el7.x86_64.rpm | cpio -idmv

3.3.5、替换可执行文件

首先自己想办法下载一个高版本的可执行文件,然后在解包后的临时目录(rpmfiles)中找到旧的文件,删掉替换以下就行,不做演示了

3.3.6、使用 fpm 打包

fpm 工具是 ruby 写的,又是由于众所周知的原因他么的国内 gem 不能用… 所以需要先搞好一个 ruby 环境,并安装上 fpm,ruby 安装参考 如何快速正确的安装 Ruby, Rails 运行环境,以下是一段安装 rvm,借助 rvm 安装 ruby 和 fpm 的脚本

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 直接从我原来的脚本里 copy 的
echo -e "\033[32minstall rvm...\033[0m"
PATH=$PATH:/usr/local/rvm/bin:/usr/local/rvm/rubies/ruby-2.3.0/bin
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 > /dev/null 2>&1
curl -sSL https://get.rvm.io | bash -s stable > /dev/null 2>&1
echo "ruby_url=https://cache.ruby-china.org/pub/ruby" >> /usr/local/rvm/user/db
rvm requirements > /dev/null 2>&1

echo -e "\033[32minstall ruby...\033[0m"
rvm install 2.3.0 > /dev/null 2>&1
rvm use 2.3.0 --default > /dev/null 2>&1

echo -e "\033[32minstall bundler...\033[0m"
gem install bundler > /dev/null 2>&1

echo -e "\033[32minstall fpm...\033[0m"
gem install fpm > /dev/null 2>&1

最后一步开始封包,封包之前确认好两件事: rpm 解包后已经替换好了相关文件、prep 段脚本已经提取好了并保存至文件中;然后就一条命令封包

1
2
# $version 指定新的版本,后面的 etc、usr 等目录就是官方 rpm 解包的文件夹
fpm -s dir -t rpm -n "etcd" -v $version --pre-install preinstall.sh --post-install postinstall.sh --pre-uninstall preuninstall.sh --post-uninstall postuninstall.sh etc usr var

四、其他相关

关于 fpm 的命令参数以及使用可以参考 github 上的 wiki,地址 点这里

以上制作过程可以自己写一个编译脚本,方便以后使用,可以参考我的 k8s、etcd、flannel rpm 制作脚本在自己写脚本时候有些坑,比如 prep 段如果官方包的 prep 脚本中包含 $ 等特殊字符,那么在自己写的编译脚本中需要将他转义处理,否则会造成引用当前编译脚本的变量,最终在 rpm 中体现不正确的情况;自己编译好 rpm 后最好同样提取下 prep 脚本并跟官方的对比一下;同时如果自己编译时是用的 root 用户那么还要注意一下权限问题,比如 k8s 的 rpm 安装后创建 kube 用户,其可执行文件都以 kube 用户执行,这时候你 root 封包的一些文件 kube 用户可能没法读取,解决办法是在 prep 段中自己加入更改权限的 shell 脚本


基于官方 rpm 快速创建自定义 rpm
https://mritd.com/2016/09/13/quickly-create-custom-rpms-based-on-official-rpm/
作者
Kovacs
发布于
2016年9月13日
许可协议