Ubuntu 上用 fpm 一次打出 deb/rpm 双格式、x86/arm 双架构安装包
需求一句话:手里只有一份「编译好的二进制」,目标客户机器上既有 x86_64 也有 arm64,既要出 deb 也要出 rpm,而且分发时希望每个包上都明确标记架构。本文记录完整方案与踩坑实录。
一、背景与需求
产品是 Java + 本地引擎混合架构的服务,引擎是 C/C++ 编译的原生二进制(必须按 CPU 架构分别编译),而 Java 部分和配置文件是架构无关的。打包前我们拿到的是:
bash
1build/2├── engine_x86_64 # x86_64 编译的引擎(15MB)3└── engine_aarch64 # arm64 编译的引擎(15MB)
需求拆开是四件事:
- 双格式:deb(Debian/Ubuntu 系)+ rpm(RedHat/CentOS 系)都要;
- 双架构:x86_64 和 arm64 都要支持;
- 包要带架构标记:实施/分发时按目标机器架构选包,一眼能认出来;
- 在 Ubuntu 上完成打包:没有 CentOS/Fedora 打包机。
先给结论:这一切用 fpm 一个工具就能搞定,核心就两件事------搞懂架构命名口径、写一个循环脚本。
二、先搞懂:同一个架构,有三套名字
这是最容易懵的地方。同一个物理架构,在 uname、deb、rpm 三个体系里各有各的叫法:
| 物理架构 | uname -m |
deb 体系(Debian/Ubuntu) | rpm 体系(RedHat/CentOS) |
|---|---|---|---|
| Intel/AMD 64位 | x86_64 |
amd64 |
x86_64 |
| ARM 64位 | aarch64 |
arm64 |
aarch64 |
为什么不对齐? 历史原因:
- x86-64 指令集由 AMD 率先推出,正式名 AMD64 ,所以 Debian 系用
amd64;RedHat 系直接沿用uname -m的输出x86_64; - ARM 64 位指令集官方名 AArch64 ,RedHat 系用
aarch64,Debian 系简化成arm64。
坑点 :这些名字是包管理器各自"法定"的,不能混用------deb 里写 x86_64,dpkg 不认识;rpm 里写 amd64,rpm 直接拒绝。所以打包时要做一次映射:
css
1# deb 用 Debian 规范名,rpm 用 RPM 规范名2declare -A DEB_ARCH=( [x86]=amd64 [arm]=arm64 )3declare -A RPM_ARCH=( [x86]=x86_64 [arm]=aarch64 )
三、核心设计:运行时按 uname -m 自动选引擎
我们的启动脚本 run.sh 有一个很省事的设计------不写死引擎文件名,运行时用 uname -m 拼:
ini
1SYSTEM_ARCH=$(uname -m)2# x86 机器上 SYSTEM_ARCH=x86_64,arm 机器上是 aarch643nohup ${BASE_DIR}/bin/core/myapp_${SYSTEM_ARCH} start &
对应地,bin/core/ 下放两个引擎,命名必须 与 uname -m 输出严格一致:
bash
1bin/core/2├── myapp_x86_64 # uname -m 输出 x86_64 → 命中3└── myapp_aarch64 # uname -m 输出 aarch64 → 命中
这个设计带来的好处:安装目录里可以同时放两套架构的引擎,装到哪台机器都能跑------也就有了下面两种打包思路。
细节:
uname -m在 32 位系统上会输出i686/armv7l。如果引擎只有 64 位版本,32 位机器装不上(deb 的amd64/arm64架构标签会在安装时直接拦截),这在分发上是"装不上比装错更安全"。
四、方案抉择:单包双架构 vs 四包标记架构
有了"目录里双引擎共存"的能力,就有两种打包路线:
方案 A:单包双架构(1 个包全平台通吃)
- deb 用
Architecture: all,rpm 用Architecture: noarch(架构无关包) - 一个包发出去,x86/arm 都能装,运行时各自选引擎
优点 :只维护 1 个安装包,分发极简;缺点:包不体现目标架构,仓库/实施流程如果按架构筛选就不好办。
方案 B:四包标记架构(最终采用)
- deb × (amd64 / arm64) + rpm × (x86_64 / aarch64) = 4 个包
- 内容完全相同(都含双引擎),仅架构标签不同
csharp
1out/2├── myapp_1.0.0_x86.deb # Architecture: amd64 → 发给 x86 客户3├── myapp_1.0.0_arm.deb # Architecture: arm64 → 发给 arm 客户4├── myapp-1.0.0-1.x86_64.rpm # Architecture: x86_645└── myapp-1.0.0-1.aarch64.rpm # Architecture: aarch64
优点 :架构标签明确,分发、归档、审计都清晰;缺点:要出 4 个包(但脚本循环一次搞定)。
实际项目中最终选了方案 B------客户现场实施人员更习惯"按机器架构拿对应包",4 个包由脚本自动产出,成本几乎为零。
五、实战:fpm 一键四包
5.1 安装 fpm
1sudo apt update2sudo apt install -y ruby ruby-dev rubygems build-essential rpm3sudo gem install fpm
公司内网 gem 可能拉不动,换清华源:
arduino1sudo gem sources --remove https://rubygems.org/2sudo gem sources --add https://mirrors.tuna.tsinghua.edu.cn/rubygems/3sudo gem install fpm
rpm包也要装------fpm 打 rpm 时依赖系统的rpmbuild(见踩坑一)。
5.2 素材目录
素材按安装路径组织(装到 /opt/myapp),fpm 打包时 --prefix / + 输入 opt 即可:
arduino
1staging/2└── opt/3 └── myapp/4 ├── bin/5 │ ├── core/6 │ │ ├── myapp_x86_647 │ │ ├── myapp_aarch648 │ │ └── myapp.properties9 │ └── lib/ # 词库/数据,架构无关10 ├── config/ # 配置文件11 └── script/12 └── run.sh # 启动脚本,运行时自动选引擎
5.3 一键打包脚本(核心)
matlab
1#!/bin/bash2set -euo pipefail34APP=myapp5VER=1.0.06DESC="myapp by example"7SRC="${1:-$(pwd)}" # 素材根(含 opt/ 的那层)8OUT="${2:-$SRC/out}"9BASE="$SRC/opt/myapp"1011# 0) 环境:fpm + rpmbuild 分别检查(root 直接执行,普通用户 sudo)12SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo"13command -v fpm >/dev/null || { $SUDO apt install -y ruby ruby-dev rubygems build-essential; $SUDO gem install fpm; }14command -v rpmbuild >/dev/null || { $SUDO apt install -y rpm; } # 注意:独立检查!1516# 1) 校验素材:用 file 确认引擎架构没放反17file "$BASE/bin/core/myapp_x86_64" | grep -qi "x86-64" || { echo "x86 引擎不对"; exit 1; }18file "$BASE/bin/core/myapp_aarch64" | grep -qi "aarch64" || { echo "arm 引擎不对"; exit 1; }1920# 2) 权限(Windows 拷来的文件没有执行位,必须补)21chmod 755 "$BASE/bin/core/myapp_"* "$BASE/script/run.sh"22chmod 644 "$BASE/bin/core/myapp.properties" "$BASE/config/"*23sed -i 's/\r$//' "$BASE/script/run.sh" # 顺手清 CRLF2425# 3) 安装后脚本:赋权/初始化(可选)26cat > "$SRC/postinst" <<'EOF'27#!/bin/bash28set -e29chmod -R 777 /opt/myapp30chmod +x /opt/myapp/bin/core/*31chmod +x /opt/myapp/script/*32echo "myapp 安装成功"33EOF34chmod +x "$SRC/postinst"3536# 4) 打 4 个包:内容相同,仅架构标签不同37mkdir -p "$OUT"38declare -A DEB_ARCH=( [x86]=amd64 [arm]=arm64 )39declare -A RPM_ARCH=( [x86]=x86_64 [arm]=aarch64 )4041for key in x86 arm; do42 fpm -s dir -t deb -a "${DEB_ARCH[$key]}" -n "$APP" -v "$VER" \43 --description "$DESC" \44 -C "$SRC" --prefix / --after-install "$SRC/postinst" \45 -p "$OUT/${APP}_${VER}_${key}.deb" opt46 fpm -s dir -t rpm -a "${RPM_ARCH[$key]}" -n "$APP" -v "$VER" \47 --description "$DESC" \48 -C "$SRC" --prefix / --after-install "$SRC/postinst" \49 -p "$OUT/${APP}-${VER}-1.${RPM_ARCH[$key]}.rpm" opt50done
几个关键参数说明:
| fpm 参数 | 作用 |
|---|---|
-s dir / `-t deb |
rpm` |
| `-a amd64 | arm64 |
-C <dir> --prefix / |
以 <dir> 为根,安装到 /(素材里自带 opt/) |
--after-install postinst |
安装后执行脚本(deb 的 postinst / rpm 的 %post) |
-p <路径> |
指定产物文件名 |
六、踩坑实录(都是真踩过的)
坑一:Need executable 'rpmbuild' to convert dir to rpm
现象:deb 正常打出,rpm 报错。
根因 :fpm 打 rpm 必须调用系统 rpmbuild。很多教程把 rpm 装进"fpm 不存在才装"的条件里------一旦机器上 fpm 已经装好,整个判断跳过,rpmbuild 就没装上。
修复 :对 rpmbuild 做独立检查,不要挂在 fpm 的判断里:
javascript
1command -v fpm >/dev/null || { ...装 fpm... }2command -v rpmbuild >/dev/null || { sudo apt install -y rpm; }
坑二:Windows 拷来的文件丢权限 + CRLF
现象 :包打出来了,装完启动脚本报 Permission denied;或 bash 脚本报 $'\r': command not found。
根因:素材从 Windows 复制到 Linux,① 文件没有执行位(Windows 无 chmod 概念);② 文本文件带 CRLF 换行,bash 不认。
修复:
perl
1chmod 755 引擎、脚本2sed -i 's/\r$//' run.sh # 清 CRLF
坑三:root 环境下 sudo 的兼容
打包机如果是 root 登录,脚本里写死 sudo apt 在个别精简系统上会找不到 sudo。用 id -u 判断统一提权前缀:
bash
1SUDO=""; [ "$(id -u)" -eq 0 ] || SUDO="sudo"2$SUDO apt install -y rpm
坑四:gem 安装超时
内网环境 gem install fpm 大概率超时,换清华源即可(见 5.1)。脚本里可以做失败自动切换。
七、打包后的验证
javascript
1# 元数据:确认架构标签2dpkg-deb -I myapp_1.0.0_x86.deb | grep Architecture # 应输出 amd643rpm -qpi myapp-1.0.0-1.aarch64.rpm | grep Architecture # 应输出 aarch6445# 内容:确认两个引擎都在包内6dpkg-deb -c myapp_1.0.0_x86.deb | grep myapp_ # 应看到 x86_64 + aarch647rpm -qpl myapp-1.0.0-1.aarch64.rpm | grep myapp_89# 安装实测(x86 机器装 x86 包)10sudo dpkg -i myapp_1.0.0_x86.deb11/opt/myapp/script/run.sh status1213# arm 无真机时用容器验证14docker run --rm --platform linux/arm64 -v $PWD:/pkg ubuntu:22.04 \15 bash -c "dpkg -i /pkg/myapp_1.0.0_arm.deb && /opt/myapp/script/run.sh status"1617# rpm 用 rockylinux 容器验证18docker run --rm -v $PWD:/pkg rockylinux:9 \19 bash -c "dnf install -y /pkg/myapp-1.0.0-1.x86_64.rpm && /opt/myapp/script/run.sh status"
八、小结
- 架构命名 :deb 用
amd64/arm64,rpm 用x86_64/aarch64,各体系法定规范名,不可混用; - 一包双架构 :启动脚本用
uname -m运行时拼引擎文件名,目录里就能同时放两套引擎; - 四包 vs 单包:架构无关包(all/noarch)省事,但分发/审计场景下"四包带标签"更清晰,脚本循环成本为零;
- fpm 要点 :
-a打标签、--prefix /配素材自带opt/、--after-install挂 postinst; - 最容易翻车的三件事 :
rpmbuild独立检查、Windows 文件的执行位与 CRLF、root/sudo 兼容。
最后一句真心话:fpm 本身很简单,难的全是"环境细节"------而这正是写进脚本、一次搞定、永不再踩的价值。