先讲一个很多人都遇到过的场景:本地 Mac 上 Electron 项目打包一切正常,electron-builder 跑完 --mac 目标没有任何问题;可一旦把构建挪到 Linux 的 CI 上,执行 electron-builder --linux deb,要么卡在某个下载步骤迟迟不动,要么直接抛一串 gem 相关的报错。你第一反应可能是配置写错了,但检查半天发现根本不是配置问题,而是 electron-builder 在首次构建 deb/rpm 包时需要临时安装一个叫 fpm 的打包工具,而这个过程默认走 rubygems.org,网络环境一差就原形毕露。
这篇文章就是来解决这一件事:怎么让 electron-builder 从你指定的 fpm 镜像地址下载 fpm,而不是傻乎乎去连 rubygems.org。内容不深,但很实用,适合在本地 Linux、Docker 或 CI 上打 Linux 安装包的 Electron 开发者。我会把背后的依赖链路、镜像配置方法、系统级 bypass 方案,以及踩过的坑一起讲清楚。
1. 为什么 electron-builder 打个 deb 包还要跟 fpm 纠缠不清
1.1 fpm 到底是什么
fpm 的全称是 Effing Package Management,作者是 Jordan Sissel,这哥们也是 Logstash 的作者。简单说,fpm 是一个用 Ruby 写的“把任意目录变成系统安装包”的命令行工具。你给它一个目录、一套文件清单,它就能输出 deb、rpm、tar.gz 等格式,省去你手写 debian 规则文件或 spec 文件的痛苦。
在 electron-builder 的 Linux 打包流程里,fpm 承担的是“生成 deb/rpm/pacman 安装包”的脏活累活。electron-builder 把应用二进制、图标、desktop 文件、依赖库等资源全部准备好之后,最后一步就是调用 fpm 把这些资源封装成发行版能识别的包格式。换句话说,fpm 是 electron-builder 在 Linux 安装包目标上的“外包工人”。
1.2 electron-builder 在哪个环节召唤 fpm
electron-builder 的 linux.target 支持很多目标,比如 AppImage、snap、deb、rpm、pacman 等。但注意,AppImage 和 snap 走的是各自的工具链,并不依赖 fpm;只有 deb、rpm、pacman 这些传统发行版包格式,才需要 fpm 参与。
所以很多人会有一种错觉:我在本地 macOS 打包时一切正常,为什么到了 Linux CI 上就非要装 fpm?因为你在 macOS 上打的通常是 dmg/zip 目标,不会触发 fpm;一旦切到 --linux deb,electron-builder 检测到系统里没有 fpm,就开始尝试自动安装。
这是我见过最多人困惑的“隐性依赖”:electron-builder 本身是 Node 工具,但 deb/rpm 这个目标却绑定了一个 Ruby 生态的二进制工具。你可以在构建日志里看到类似 fpm -s dir -t deb -n 你的应用名 -v 版本号 这样的命令,看到它基本就说明 fpm 已经被调用了。
1.3 默认安装链路:为什么慢、为什么失败
electron-builder 的默认行为是:如果系统 PATH 里找不到 fpm,它会尝试用系统的 gem 去安装,命令大致等同于:
bash复制gem install fpm --no-document
这行命令会连到 rubygems.org 下载 fpm 及其一堆依赖,比如 clamp、cabin、mustache、arr-pm、backports、dotenv、pleaserun 等。国内网络访问 rubygems.org 的稳定性,体验过的朋友都清楚:有时候是超时,有时候连证书校验都过不去,运气好可能几分钟拉完,运气不好直接卡死然后报 Gem::RemoteFetcher::FetchError。
关键点在于:electron-builder 不会自己内嵌一个 RubyGems 镜像源,它执行 gem 命令时完全继承当前系统的 RubyGems 配置。 所以自定义 fpm 镜像地址的思路就非常明确了:要么在 gem 层面把源换掉,让 electron-builder 的自动安装也走镜像;要么直接把 fpm 提前装好,然后告诉 electron-builder 别折腾了,用系统现成的。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 先确诊:你的构建到底卡在哪一步
2.1 三种典型报错和它们的含义
改配置之前,建议你先看一眼构建日志,判断问题是不是真的出在 fpm 依赖链上。我把常见的三种故障形态列出来,方便对照:
| 报错特征 | 真实含义 |
|---|---|
日志里出现 Cannot find fpm、fpm: command not found |
electron-builder 没有找到 fpm,也没有成功自动安装 |
日志里出现 Gem::RemoteFetcher::FetchError、timed out、Connection reset by peer |
gem 在下载 fpm 依赖时网络连接 rubygems.org 失败 |
日志里出现 SSL_connect returned=1 或证书校验失败 |
当前 RubyGems 源证书不可信,常见于代理或公司内网抓包场景 |
如果你的日志里根本没有出现 fpm 相关的词,那就要换个方向排查了,别急着套下面的方案。
2.2 手动检查环境里的 fpm 与 gem 状态
在构建机上先手动检查一下,比反复跑 electron-builder 快得多:
bash复制which fpm
fpm --version
gem list fpm
gem sources -l
如果 which fpm 有输出,说明系统里已有 fpm,那你直接跳到第 4 章的方案二,让 electron-builder 用系统 fpm 即可。如果 gem list fpm 显示为空,而 gem sources -l 里只有 https://rubygems.org/,那大概率是 fpm 根本没有成功装到系统里,后面的自动安装也会反复失败。
2.3 用调试日志还原 electron-builder 的真实行为
electron-builder 的日志默认比较简洁,只看默认输出往往看不到它执行了哪些 gem 命令。这时候可以开调试日志:
bash复制DEBUG=electron-builder npx electron-builder --linux deb --publish never
开 debug 之后,你会在输出里看到 electron-builder 是否在尝试 gem install fpm,以及它把 fpm 放到了哪个缓存目录。我有一次排查一个问题,发现 electron-builder 在 /root/.cache/electron-builder/fpm 下维护了一份 fpm,而不是系统 gem 目录,导致我手动装好的 fpm 版本始终没有生效。这时候的解决思路就要用到第 4 章的方案二了。
3. 方案一:改动 RubyGems 源,让 electron-builder 自动走镜像
3.1 推荐镜像站与选型建议
自定义 fpm 镜像地址,落实到操作层面其实就是“改 RubyGems 源”。electron-builder 自动安装 fpm 时,会读取系统 gem 配置,所以我们先帮 gem 配置一个靠谱的源。
国内常用的 RubyGems 镜像源有这几个:
| 镜像站 | 地址 | 备注 |
|---|---|---|
| Ruby China | https://gems.ruby-china.com/ | 老地址 gems.ruby-china.org 已停用,别再用错了 |
| 清华 TUNA | https://mirrors.tuna.tsinghua.edu.cn/rubygems/ | 教育网环境表现很好 |
| 腾讯云 | https://mirrors.cloud.tencent.com/rubygems/ | 腾讯云内网构建机优先考虑 |
选哪个没绝对标准,我的建议是:如果你在腾讯云/AWS 等云上的 CI 构建,优先用云厂商自己的镜像;如果是在公司内网,优先问一下有没有自建的 RubyGems 仓库,稳定性和内网速度通常碾压公网镜像。
3.2 通过 gem 命令修改源(最常用)
最直接的方式就是用 gem sources 命令把默认源换掉:
bash复制gem sources --add https://gems.ruby-china.com/
gem sources --remove https://rubygems.org/
注意顺序不要反了,先添加镜像,再移除官方源。移除之后可以用 gem sources -l 确认为:
bash复制*** CURRENT SOURCES ***
https://gems.ruby-china.com/
这里有一个很容易踩的坑:如果你只是加了镜像,没有移除官方源,gem 默认会按列表顺序尝试,官方源排在前面的情况下还是会先去连 rubygems.org,可能继续卡死。建议在构建机上只保留一个可用源,不要心软留两个。
3.3 通过 .gemrc 持久化配置
如果你是长期维护的构建机或 CI 基础镜像,可以把镜像配置写进 ~/.gemrc,这样每次开新 shell 或重启执行器都有效:
yaml复制---
:sources:
- https://gems.ruby-china.com/
保存到 ~/.gemrc 后,可以用 gem sources -l 验证。如果你的 gem 版本较新,配置文件读取路径也可能是 ~/.config/gem/gemrc,不过 ~/.gemrc 是最通用、兼容性最好的位置,优先改它。
如果你不想改全局配置,也可以临时指定一个 gemrc 文件:
bash复制GEMRC=/path/to/my.gemrc npx electron-builder --linux deb --publish never
这个方式很适合 CI 里多个项目共用一个构建机、却又想各自指定镜像的场景。
3.4 验证源是否生效
改完源之后,最稳的验证方式是先手动安装一次 fpm:
bash复制gem install fpm --no-document
如果镜像源配置正确,你应该能看到下载速度明显改善,并且最终出现类似 Successfully installed fpm-1.15.1 的信息。手动装成功后,再跑 electron-builder 的 deb 目标,自动安装环节就不会再触发网络问题了。
当然,如果你不想手动装,也可以直接删掉 electron-builder 的 fpm 缓存再跑一次,观察它的自动安装是否走新源:
bash复制rm -rf ~/.cache/electron-builder/fpm
npx electron-builder --linux deb --publish never
4. 方案二:系统里预装好 fpm,绕过 electron-builder 的自动下载
4.1 手动安装 fpm 的正确姿势
方案二的核心思路很直接:不等 electron-builder 自动安装,你先用镜像源把 fpm 装好,然后让它直接用。好处是版本可控、失败点明确、重复构建不用再试探网络。
手动安装同样要先处理好源,然后指定版本和源地址:
bash复制gem install fpm --source https://gems.ruby-china.com/ --no-document
如果你想固定版本,比如希望所有构建机器都使用同一个 fpm 版本:
bash复制gem install fpm -v 1.15.1 --source https://gems.ruby-china.com/ --no-document
固定版本在 CI 流水线上比较重要。electron-builder 对 fpm 的版本虽然包容度很高,但不同 fpm 版本打出来的 deb 包里的脚本行为可能有细微差异,版本不一致容易出现“本地能装、CI 上装不上”的玄学问题。
4.2 用 USE_SYSTEM_FPM=true 告诉 electron-builder 别折腾
系统里装好 fpm 之后,还需要让 electron-builder 跳过自动安装逻辑。这里用到一个环境变量:
bash复制export USE_SYSTEM_FPM=true
然后正常执行打包:
bash复制npx electron-builder --linux deb --publish never
如果你不想全局导出,也可以在命令前临时加:
bash复制USE_SYSTEM_FPM=true npx electron-builder --linux deb --publish never
设置这个变量后,electron-builder 会从系统 PATH 里找 fpm,不再执行 gem install。验证方式也很简单,看构建日志里有没有出现下载 fpm 相关字样,或者在打包前直接确认:
bash复制which fpm
只要 which fpm 有输出,且你设置了 USE_SYSTEM_FPM=true,构建就应该非常安静地直接进入打包环节。
有一点要提醒:不同 electron-builder 版本对这个环境变量的命名可能有细微差异。我在 24、25 版本上用的是 USE_SYSTEM_FPM=true,早期 20.x 时代也见过社区用 ELECTRON_BUILDER_USE_SYSTEM_FPM。为了保险起见,我建议两个都设置:
bash复制export USE_SYSTEM_FPM=true
export ELECTRON_BUILDER_USE_SYSTEM_FPM=true
反正多设置一个不亏,环境变量不会互相干扰。
4.3 这个方案的适用场景与隐藏要求
方案二最大的优点是把“网络不稳定”这个变量从构建链路里彻底移除。日常开发机、Docker 基础镜像、CI runner 镜像,都适合用“预装 fpm + 环境变量跳过”的模式。
但它也有一个隐藏要求:构建环境里必须有 Ruby 和 RubyGems。如果你的机器是刻意精简的容器,连 Ruby 都没有,那方案一其实也跑不起来,因为 electron-builder 自动安装 fpm 也是基于系统 gem。这时候要么装 Ruby,要么直接用带 Ruby 的 electron-builder Docker 镜像,比如 electronuserland/builder 系列,它里面一般已经预置了 fpm 相关环境。
还有一个细节:如果你的系统里已经有多个 Ruby 版本,比如 rbenv 或 rvm 管理,务必要确认 gem 和 fpm 在同一个 PATH 里。我之前遇到过一个情况,手动装的 fpm 在 /usr/local/bin/fpm,但 electron-builder 找到的 gem 是另一个 Ruby 环境,导致 gem list fpm 查不到任何包,看起来就像系统里没有 fpm。
5. CI/CD 与内网环境:把镜像配置固化进构建链路
5.1 GitHub Actions 中的配置示例
如果你的构建跑在 GitHub Actions 上,最省心的方式是在 workflow 里把镜像源和 fpm 安装写进构建步骤:
yaml复制- name: Setup RubyGems mirror
run: |
gem sources --add https://gems.ruby-china.com/
gem sources --remove https://rubygems.org/
gem install fpm --no-document
env:
USE_SYSTEM_FPM: true
- name: Build deb package
run: npx electron-builder --linux deb --publish never
env:
USE_SYSTEM_FPM: true
注意,USE_SYSTEM_FPM: true 只需要在打包步骤里设置,前面步骤主要是为了手动装好 fpm。如果你觉得每次都给 GitHub Action 装一遍 fpm 太慢,可以加上缓存,后面第 5.3 节会讲。
5.2 私有化构建机与本地 gem 镜像
如果你们公司用的是私有化构建机,或者 OpenStack/K8s 上的动态运行器,我更推荐把镜像配置直接写进基础镜像的 /etc/gemrc 里,而不是每次构建时临时执行一段脚本。这样所有基于该镜像启动的容器自动就走内网或云厂商镜像源。
内网场景还有一个进阶玩法:用 geminabox 或 nexus 自建一个 RubyGems 私有仓库,把 fpm 及其依赖预缓存进去。此时 .gemrc 里指向的就不是公网镜像,而是你们自己的仓库地址:
yaml复制---
:sources:
- http://gems.internal.example.com/
这在银行、政企、军工这些网络隔离环境里非常实用。fpm 本身是开源工具,依赖链也不复杂,一次性推进内部仓库后,所有项目打包都稳了。不过要注意,内网仓库需要保证 RubyGems 协议兼容,建议用一个固定版本的多试几轮再全量推广。
5.3 缓存策略:让重复构建不再重复下载
无论用哪个方案,重复构建时最大的耗时来源其实是“反复下载”。如果你的 CI 用的是 GitHub Actions、GitLab CI 或 Jenkins,强烈建议把 gem 缓存和 electron-builder 缓存都挂起来。
GitHub Actions 的缓存示例:
yaml复制- name: Cache fpm and electron-builder
uses: actions/cache@v3
with:
path: |
~/.gem
~/.cache/electron-builder
~/.bundle
key: ${{ runner.os }}-fpm-${{ hashFiles('**/package.json') }}
这里缓存 ~/.gem 可以直接跳过 gem install 的下载过程,缓存 ~/.cache/electron-builder 则能同时跳过 electron 二进制和 fpm 的重复拉取。第一次构建可能还是慢,但第二次开始基本秒过。
Jenkins 或 GitLab CI 上思路一样,把这两个目录挂到持久化卷上即可。我见过不少团队只缓存了 node_modules 和 electron 缓存,漏掉了 ~/.gem,结果 f
