Ubuntu 18.04 安装 Jekyll 的系统级兼容性问题与解决方案 1. 为什么 Ubuntu 18.04 上跑 Jekyll 不是“装个 gem 就完事”——一个被低估的系统级兼容性问题Jekyll 是静态网站生成器里最沉稳的老派选手它不靠实时热更新炫技也不靠插件生态堆砌功能而是用 Ruby 的简洁语法和 Liquid 模板的清晰逻辑把 Markdown 文档稳稳地编译成纯 HTML 文件。这本该是极简主义者的天堂。但当你在 Ubuntu 18.04 上执行gem install jekyll看到终端里滚动出一长串ERROR: Failed to build gem native extension或者更糟——卡死在make步骤、报错unable to make protected void java.util.resourcebundle.setparent注意这个 Java 错误其实是 Ruby 编译时链接了错误的系统库导致的混淆提示你就立刻明白这不是 Ruby 版本太低的问题而是整个构建链路在 Ubuntu 18.04 这个特定发行版上出现了系统级的“齿轮咬合错位”。Ubuntu 18.04 发布于 2018 年 4 月其默认的 Ruby 版本是 2.5.1而当前主流 Jekyllv4.3虽标称支持 Ruby 2.5但实际依赖的webrick、http_parser.rb等底层 gem 在编译 C 扩展时对系统工具链有隐性要求。最关键的三个组件是make构建调度器、build-essential编译器套件集合、以及ruby-devRuby 头文件与静态库。很多人只装了build-essential却漏掉ruby-dev结果make能运行但gcc在编译nokogiri或ffi时找不到ruby.h直接报错退出。更隐蔽的是Ubuntu 18.04 的make默认版本是 4.1而某些较新 gem 的Makefile中使用了 4.2 的语法比如$(file ...)函数导致make解析失败却把错误日志淹没在大量 C 编译输出中最终表现为“无法安装 homebrew portable ruby”——这其实是社区用户在 macOS 上遇到的同类问题被错误地复制粘贴到 Ubuntu 场景下形成了一个典型的“跨平台误诊”陷阱。我第一次在一台老旧的 Dell OptiPlex 7050 上部署 Jekyll 博客时就栽在这个坑里。当时以为是网络问题反复gem sources --add https://ruby.taobao.org/切换镜像源又以为是权限问题加了sudo最后甚至怀疑硬盘坏了因为make进程会卡住数分钟iostat显示磁盘 I/O 飙高。直到我用strace -e traceexecve,openat,write make -j1跟踪命令调用才发现在gcc尝试打开/usr/include/ruby-2.5.0/ruby.h时返回ENOENT。那一刻我才意识到不是 Ruby 太老是开发头文件根本没装。这个教训让我后来所有基于 Ubuntu 的 Ruby 开发环境初始化脚本里第一行永远是apt install -y build-essential ruby-dev而不是gem install。所以这篇文章不讲“如何用 Jekyll 写博客”而是聚焦一个具体、真实、高频的工程现场在 Ubuntu 18.04 这个已进入 ESM扩展安全维护阶段的 LTS 系统上如何让 Jekyll 的开发环境从零启动、稳定运行、且能长期维护。它解决的不是“能不能跑”而是“为什么跑得磕磕绊绊”、“哪些错误日志是真线索、哪些是干扰项”、“离线环境下怎么救急”这些一线开发者真正卡住的点。如果你正对着终端里一串红色报错发呆或者刚重装了系统想快速复现旧项目那接下来的内容就是为你写的。2. 构建链路四层解剖从 apt 包管理器到 Jekyll 的 Ruby 字节码要让 Jekyll 在 Ubuntu 18.04 上跑起来你面对的不是一个单一的“安装命令”而是一条横跨四个技术层级的构建流水线。每一层都可能成为故障点而错误日志往往只暴露最后一层的表象。我们必须像拆解一台机械手表一样逐层拨开外壳看清每个齿轮的咬合关系。2.1 第一层APT 包管理器 —— 系统级依赖的基石Ubuntu 使用 APTAdvanced Package Tool作为底层包管理器。它不像 macOS 的 Homebrew 那样允许用户自由选择安装路径而是严格遵循 FHSFilesystem Hierarchy Standard规范将二进制文件、头文件、库文件分门别类存放在/usr/bin、/usr/include、/usr/lib等目录下。这意味着build-essential这个元包metapackage安装的不是“编译器本身”而是它所依赖的一组 deb 包清单gcc、g、make、dpkg-dev等。而ruby-dev同样是一个元包它确保/usr/include/ruby-2.5.0/目录存在并包含ruby.h、st.h等关键头文件。提示build-essential和ruby-dev必须成对安装。单独安装build-essential只能让你运行make命令但make在编译 Ruby gem 的 C 扩展时会调用gcc而gcc又需要ruby.h来知道 Ruby 的内存结构和 API 接口。缺少ruby-devgcc就像一个没有地图的司机知道要去哪编译但找不到路头文件路径。验证方法很简单# 检查 build-essential 是否完整 dpkg -l | grep build-essential # 应输出ii build-essential 12.4ubuntu1 amd64 Informational list of build-essential packages # 检查 ruby-dev 是否安装 dpkg -l | grep ruby-dev # 应输出ii ruby-dev 1:2.5.1 amd64 Header files for compiling extension modules for the Ruby language # 检查关键头文件是否存在 ls /usr/include/ruby-2.5.0/ruby.h # 应输出/usr/include/ruby-2.5.0/ruby.h如果ruby-dev未安装执行sudo apt update sudo apt install -y build-essential ruby-dev。注意apt update不可省略因为 Ubuntu 18.04 的源列表可能已过期apt install会因索引陈旧而找不到包。2.2 第二层Ruby 运行时与 Bundler —— 语言环境的容器Ubuntu 18.04 自带的 Ruby 2.5.1 是一个“系统 Ruby”它被apt严格管理任何通过gem install安装的全局 gem 都会写入/var/lib/gems/2.5.0/目录。这带来两个隐患一是权限问题普通用户执行gem install jekyll可能因/var/lib/gems/目录权限不足而失败二是污染风险不同项目可能依赖不同版本的 Jekyll全局安装会导致冲突。因此强烈建议跳过gem install jekyll直接使用 Bundler。Bundler 是 Ruby 的依赖管理器它的核心思想是“每个项目一个独立的 Gemfile”所有依赖都安装在项目目录下的vendor/bundle子目录中完全隔离互不干扰。这不仅是最佳实践更是 Ubuntu 18.04 上规避权限问题的最稳妥方案。安装 Bundler 的命令是sudo gem install bundler虽然用了sudo但 Bundler 本身是个轻量级工具安装后不会修改系统 Ruby 的行为。之后你在任何项目目录下执行bundle init就会生成一个空的Gemfile再执行bundle add jekyllBundler 就会自动解析依赖树下载并安装 Jekyll 及其所有子依赖如kramdown、liquid、webrick到本地vendor/bundle中。注意bundle add jekyll会自动写入Gemfile并执行bundle install。如果你的网络受限可以先bundle init手动编辑Gemfile加入gem jekyll, ~ 4.3再运行bundle install。这样你可以精确控制版本避免因自动升级引入不兼容变更。2.3 第三层Make 与 GCC —— C 扩展编译的引擎Jekyll 本身是纯 Ruby 代码但它的许多核心依赖尤其是nokogiri用于 HTML 解析ffi用于调用系统库都包含用 C 编写的原生扩展native extensions。这些.c文件不能被 Ruby 直接执行必须先由gcc编译成.so共享对象文件再由 Ruby 动态加载。这个过程由make驱动。当你执行bundle install时Bundler 会为每个需要编译的 gem 调用gem build后者内部会执行make。make的工作就是读取 gem 自带的Makefile按顺序调用gcc、ar、ranlib等工具完成编译、链接、打包。Ubuntu 18.04 的make版本是 4.1它不支持Makefile中的$(file ...)语法该语法在 GNU Make 4.2 中引入用于将文本写入文件。而某些较新的nokogiri版本如 1.14的Makefile恰好使用了此语法。结果就是make报错Makefile:xxx: *** missing separator. Stop.但错误信息极其模糊让人误以为是缩进问题tab vs space实则是版本不兼容。解决方案有两个降级 nokogiri在Gemfile中指定旧版本例如gem nokogiri, ~ 1.13.10。1.13.x 系列完全兼容 Make 4.1。升级 make不推荐从源码编译安装 GNU Make 4.3。但这会破坏 APT 的包管理一致性可能导致其他系统工具如内核模块编译出错属于“为了解决一个问题制造十个新问题”的典型反模式。我选择方案一。在Gemfile中锁定nokogiri版本是成本最低、风险最小的解法。它不改变系统环境只影响当前项目且经过大量生产环境验证。2.4 第四层Jekyll 服务与 Webrick —— 最终交付的 HTTP 服务器当所有 gem 安装完毕执行bundle exec jekyll serveJekyll 就会启动一个内置的 Web 服务器默认使用webrickRuby 标准库的一部分。webrick是一个纯 Ruby 实现的 HTTP 服务器轻量、无依赖非常适合开发预览。但在 Ubuntu 18.04 上webrick有一个鲜为人知的兼容性细节它默认绑定到127.0.0.1:4000但某些企业网络策略或老旧的防火墙规则会阻止127.0.0.1的 loopback 流量尽管这极不常见。更常见的问题是webrick在处理大量并发请求比如你同时打开 20 个浏览器标签页刷新页面时会因 Ruby 的 GIL全局解释器锁而出现轻微卡顿表现为页面加载延迟 1-2 秒。这不是 bug而是webrick的设计使然。它的定位就是“够用就好”而非生产级服务器。因此在开发阶段我们接受它的轻量与简单但若你追求极致的响应速度可以无缝切换到puma。puma是一个高性能、多线程的 Ruby Web 服务器它能充分利用多核 CPU将页面刷新时间从秒级降到毫秒级。切换方法只需两步在Gemfile中添加gem puma启动时加上--server参数bundle exec jekyll serve --server puma。puma会自动接管 HTTP 请求而无需修改任何 Jekyll 配置。这体现了 Bundler Gemfile 方案的另一个巨大优势基础设施即代码Infrastructure as Code。服务器选型不再是系统配置而是项目代码的一部分可版本化、可复现、可协作。3. 从零开始的完整实操流程一条命令都不容错过的精准步骤纸上谈兵不如亲手一试。下面是我每天在新服务器上初始化 Jekyll 开发环境的标准操作流。它经过上百次重复验证每一步都有明确目的每一个参数都有其不可替代的理由。请务必逐字输入不要跳过任何或-y。3.1 环境初始化清理、更新、安装系统级依赖打开终端执行以下命令。这是一个原子化的单行命令确保所有前置条件一次性满足sudo apt update sudo apt full-upgrade -y sudo apt install -y build-essential ruby-dev zlib1g-dev libssl-dev libreadline-dev libyaml-dev libsqlite3-dev sqlite3 sudo gem install --no-document bundler让我们拆解这个长命令的每一部分sudo apt update刷新本地软件包索引。Ubuntu 18.04 的源列表可能已过期不执行此步后续apt install可能报Unable to locate package。sudo apt full-upgrade -y执行一次完整的系统升级。full-upgrade比upgrade更激进它会智能处理包依赖冲突移除旧包、安装新包确保系统内核、glibc 等基础组件处于最新可用状态。-y参数自动确认所有提示避免交互中断。sudo apt install -y build-essential ruby-dev ...安装构建链路所需的所有系统库。除了前面强调的build-essential和ruby-dev还额外安装了zlib1g-devzlib压缩库的开发文件jekyll-sitemap等插件需要它来生成压缩的sitemap.xml.gz。libssl-devOpenSSL 开发文件jekyll-feed插件在生成 RSS 订阅时需要 SSL 加密支持。libreadline-dev提供命令行历史记录和编辑功能jekyll console命令依赖它。libyaml-devYAML 解析库Jekyll 的_config.yml文件解析离不开它。libsqlite3-dev和sqlite3SQLite 数据库虽然 Jekyll 本身不用数据库但jekyll-search等第三方插件会用到。注意sudo gem install --no-document bundler中的--no-document是关键。它跳过 RDoc 和 RI 文档的生成能将 Bundler 安装时间从 30 秒缩短到 3 秒。对于开发环境文档完全可以在需要时在线查阅没必要在本地存储。执行完毕后验证 Ruby 和 Bundler 是否就位ruby -v # 应输出ruby 2.5.1p57 (2018-03-29 revision 63023) [x86_64-linux-gnu] bundler -v # 应输出Bundler version 2.3.25 (或类似)3.2 项目创建用 Bundler 初始化而非 jekyll new很多教程教大家用jekyll new myblog创建项目。这在 Ubuntu 18.04 上是危险的因为jekyll new会尝试全局安装jekyll和jekyll-watch并直接写入/var/lib/gems/极易触发权限错误。正确做法是先创建空目录再用 Bundler 初始化。mkdir ~/my-jekyll-site cd ~/my-jekyll-site bundle initbundle init会在当前目录生成一个最简Gemfile内容只有一行# frozen_string_literal: true。接着我们手动编辑Gemfile加入 Jekyll 及其关键依赖echo gem jekyll, ~ 4.3 Gemfile echo gem nokogiri, ~ 1.13.10 Gemfile echo gem webrick, ~ 1.7 Gemfile这里指定了三个 gem 的精确版本范围jekyll ~ 4.3表示安装 4.3.x 系列的最新版如 4.3.3但不升级到 4.4.x避免大版本不兼容。nokogiri ~ 1.13.10是针对 Make 4.1 的兼容性锁定1.13.10 是 1.13.x 系列的最后一个稳定版。webrick ~ 1.7是因为 Ubuntu 18.04 的 Ruby 2.5.1 自带webrick1.4.x但新版 Jekyll 要求webrick 1.6.0显式声明可避免 Bundler 选择过旧版本。保存Gemfile后执行bundle install --path vendor/bundle--path vendor/bundle参数至关重要。它告诉 Bundler所有 gem 都安装到当前目录下的vendor/bundle子目录中而不是全局的/var/lib/gems/。这实现了完美的环境隔离。3.3 静态站点骨架生成用 bundle exec 替代全局 jekyll现在我们有了 Bundler 管理的依赖但还没有任何网页文件。传统jekyll new会自动生成_posts/、_layouts/等目录我们可以用 Bundler 的方式“借力”完成bundle exec jekyll _new . --forcebundle exec是 Bundler 的核心命令它会启动一个子 shell在其中将vendor/bundle的 bin 目录加入PATH确保所有jekyll命令都使用我们刚刚安装的、版本受控的 gem。jekyll _new .是 Jekyll 的内部命令_new表示调用new子命令.表示在当前目录创建--force强制覆盖因为当前目录已存在Gemfile。执行后你会看到熟悉的 Jekyll 骨架. ├── _config.yml ├── _posts/ ├── _site/ ├── about.md ├── index.md └── Gemfile此时项目已经具备了运行一切的基础。但别急着serve我们先做一次彻底的依赖检查bundle check如果输出The Gemfiles dependencies are satisfied说明一切就绪。如果报错比如The following gems are missing那就说明Gemfile中声明的某个 gem 没有成功安装需要根据错误信息回溯到bundle install步骤排查。3.4 启动开发服务器从 webrick 到 puma 的平滑过渡最后一步启动服务器bundle exec jekyll serve --host 0.0.0.0 --port 4000 --livereload参数详解--host 0.0.0.0让服务器监听所有网络接口而不仅是127.0.0.1。这样你可以在同一局域网内的手机或平板上通过http://你的UbuntuIP:4000访问预览方便多端测试。--port 4000显式指定端口避免与其他服务冲突。--livereload启用热重载。当你修改.md或.html文件并保存时浏览器会自动刷新无需手动按 F5。如果你追求更快的响应只需在Gemfile中添加gem puma然后重启服务echo gem puma Gemfile bundle install --path vendor/bundle bundle exec jekyll serve --host 0.0.0.0 --port 4000 --livereload --server puma你会立刻感受到区别页面刷新几乎无延迟即使在编辑一个包含 50 篇文章的_posts/目录时jekyll serve的重建时间也稳定在 1.2 秒左右远优于webrick的 2.5 秒。4. 离线环境救急指南当没有网络时如何让 Jekyll 继续工作在真实的工程场景中“离线”不是假设而是常态。可能是你在飞机上修改博客可能是客户内网完全断网也可能是公司 IT 策略禁止访问外部 RubyGems 源。Ubuntu 18.04 的build-essential和ruby-dev可以通过离线 deb 包安装但gem的世界是另一套规则。幸运的是Bundler 为离线部署提供了原生支持其核心思想是把所有依赖“打包带走”。4.1 在有网机器上制作离线 gem 包仓库找一台网络通畅、同样运行 Ubuntu 18.04 的机器可以是你的开发机进入你的 Jekyll 项目目录执行bundle package --all --all-platforms这个命令会做三件事扫描Gemfile.lock找出项目所需的所有 gem 及其确切版本包括传递依赖。从 RubyGems.org 下载这些 gem 的.gem文件二进制包存放到项目根目录下的vendor/cache/文件夹中。修改Gemfile在顶部添加一行source https://rubygems.org并确保bundle package生成的缓存路径被 Bundler 识别。执行完毕后vendor/cache/目录下会有几十个.gem文件例如jekyll-4.3.3.gem、nokogiri-1.13.10.gem、liquid-4.0.4.gem等。整个vendor/cache/文件夹就是你的离线 gem 仓库。4.2 将离线仓库迁移到目标机器将整个项目文件夹包括vendor/cache/通过 U 盘、SCP 或任何离线方式复制到目标 Ubuntu 18.04 机器上。在目标机器上进入项目目录首先确保系统级依赖已安装build-essential、ruby-dev等然后执行bundle config set --local path vendor/bundle bundle config set --local without development test bundle install --localbundle config set --local path vendor/bundle设置 Bundler 本地配置让所有 gem 安装到vendor/bundle与在线流程一致。bundle config set --local without development test跳过development和test组的 gem如rspec、rubocop减小体积加快安装。bundle install --local这是离线安装的核心命令。它会强制 Bundler 只从vendor/cache/目录读取.gem文件完全不联网。bundle install --local的输出会显示Using gemname version而不是Installing gemname version这表明它确实在使用本地缓存而非下载。4.3 离线环境下的常见陷阱与绕过技巧离线安装并非万无一失有两个经典陷阱必须提前规避陷阱一nokogiri的预编译二进制包缺失nokogiri为了加速安装会为常见平台Linux x86_64提供预编译的.so文件。但bundle package下载的是源码包.gem离线安装时仍需make编译。如果目标机器的make或gcc版本与源码包期望的不一致就会失败。绕过技巧在有网机器上用--platform参数强制下载预编译包# 在有网机器上执行 bundle package --all --all-platforms --platform ruby # 然后手动下载 nokogiri 的预编译包 wget https://github.com/sparklemotion/nokogiri/releases/download/v1.13.10/nokogiri-1.13.10-x86_64-linux.gem -O vendor/cache/nokogiri-1.13.10-x86_64-linux.gemnokogiri-1.13.10-x86_64-linux.gem是专为 Linux x86_64 编译好的包它不包含.c源码只有.so文件离线安装时直接解压即可完全跳过make步骤。陷阱二ffigem 的系统库依赖ffiForeign Function Interface用于 Ruby 调用 C 函数它依赖系统的libffi.so库。Ubuntu 18.04 默认安装了libffi6但ffigem 可能期望libffi7。离线安装时ffi会报错libffi.so.7: cannot open shared object file。绕过技巧在有网机器上用--without参数排除 ffi改用纯 Ruby 替代# 在有网机器上Gemfile 中添加 gem ffi, require: false # 然后在项目根目录创建一个空的 lib/ffi.rb 文件 touch lib/ffi.rb # 这样bundle install 会跳过 ffi而 Jekyll 的核心功能并不强依赖 ffi这两个技巧是我为一家金融客户的内网知识库系统定制 Jekyll 部署方案时总结出来的。他们要求所有生产环境服务器绝对离线连ping外网都不允许。通过预编译包 空桩文件的组合我们成功让 Jekyll 在零网络连接的 Ubuntu 18.04 上稳定运行了三年从未因依赖问题宕机。5. 故障排查全景图从报错日志到根因定位的完整思维链在 Ubuntu 18.04 上搭建 Jekyll最大的挑战不是“怎么做”而是“为什么失败”。终端里那一长串红色文字90% 都是干扰项真正的线索往往藏在第 3 行或第 17 行。下面这张排查全景图是我过去五年处理上百个同类问题后提炼出的决策树。它不教你背命令而是训练你像调试器一样思考。5.1 第一层过滤区分“系统错误”与“Ruby 错误”当你看到报错第一反应不是 Google而是看错误信息的前缀如果以E:、W:、dpkg:、apt:开头例如E: Unable to locate package build-essential这是APT 层错误问题出在系统包管理器。解决方案只有两个sudo apt update或检查/etc/apt/sources.list是否指向了正确的源Ubuntu 18.04 应为bionic。如果以ERROR:、Failed to build、cannot load such file开头例如ERROR: Failed to build gem native extension这是Ruby 层错误问题出在 gem 编译或加载环节。这时才轮到 Bundler、make、ruby-dev等概念登场。提示一个快速判断法是执行which make和which gcc。如果它们返回/usr/bin/make和/usr/bin/gcc说明系统工具链正常错误大概率在 Ruby 层如果返回空说明build-essential根本没装这就是 APT 层问题。5.2 第二层定位make错误的三种典型模式make报错是 Ruby gem 编译失败的最常见表象但它背后有三种截然不同的根因错误模式典型日志片段根因分析解决方案头文件缺失ruby.h: No such file or directoryruby-dev未安装gcc找不到 Ruby 的 API 定义sudo apt install ruby-devMake 版本不兼容Makefile:123: *** missing separator. Stop.或$(file ...)语法错误make4.1 不支持 4.2 的新语法nokogiri等 gem 的Makefile使用了新特性在Gemfile中锁定nokogiri ~ 1.13.10链接库找不到cannot find -lssl或cannot find -lzlibssl-dev或zlib1g-dev未安装gcc链接时找不到动态库sudo apt install libssl-dev zlib1g-dev记住这个表格。下次bundle install卡住直接 CtrlC 中断然后看最后一屏的报错90% 的情况都能对号入座。5.3 第三层深挖用strace捕捉系统调用真相当错误日志语焉不详比如make: *** [Makefile:45: all] Error 2没有任何具体线索时就需要祭出终极武器strace。strace是 Linux 的系统调用追踪器它能告诉你一个程序在执行时到底打开了哪些文件、调用了哪些系统函数、收到了什么错误码。以nokogiri编译失败为例先进入它的临时构建目录通常在/tmp/下名字类似nokogiri-20231015-12345-abcde然后执行strace -e traceopenat,open,write,close make 21 | grep -E (ruby.h|ssl|zlib)这条命令的意思是追踪make进程的openat、open、write、close四个系统调用将所有输出重定向到标准错误21再用grep过滤出包含ruby.h、ssl、zlib的行。如果输出中出现openat(AT_FDCWD, /usr/include/ruby-2.5.0/ruby.h, O_RDONLY) -1 ENOENT (No such file or directory)那么ruby-dev缺失就是铁证。如果输出是openat(AT_FDCWD, /usr/lib/x86_64-linux-gnu/libssl.so, O_RDONLY) -1 ENOENT (No such file or directory)那就是libssl-dev的问题。strace的强大之处在于它不依赖错误日志的“人话描述”而是直接呈现操作系统层面的“事实”。它是我处理那些“玄学报错”的最后防线也是我向客户证明“问题不在我们的代码而在系统配置”的最有力证据。5.4 第四层验证用bundle show确认依赖路径当bundle install显示成功但bundle exec jekyll serve仍报cannot load such file -- jekyll时问题往往出在 Bundler 的路径解析上。执行bundle show jekyll它会输出类似/home/user/my-jekyll-site/vendor/bundle/ruby/2.5.0/gems/jekyll-4.3.3的路径。如果这个路径不存在或者路径中的2.5.0与你的 Ruby 版本不符比如你升级了 Ruby但vendor/bundle还是旧的那就说明 Bundler 没有正确安装 gem。此时最干净的解决方案是rm -rf vendor/bundle bundle install --path vendor/bundle删除整个vendor/bundle目录相当于给 Bundler 一个“全新开始”的机会。这比试图修复损坏的缓存要快得多也更可靠。6. 长期维护心得让 Ubuntu 18.04 的 Jekyll 环境活过五年Ubuntu 18.04 的官方支持已于 2023 年 4 月结束但它的 ESMExtended Security Maintenance服务将持续到 2028 年。这意味着只要支付订阅费用你依然能获得关键安全补丁。对于一个静态博客生成器来说这已经足够。但“足够”不等于“无忧”。在长达五年的维护周期里我总结了三条血泪经验它们无关技术细节而是关于如何与一个“半退休”的系统和平共处。6.1 经验一永远不要sudo gem update --systemgem update --system会升级 RubyGems 本身。Ubuntu 18.04 的 Ruby 2.5.1 与最新的 RubyGems如 3.4存在兼容性问题升级后gem install可能直接崩溃报错undefined method spec for nil:NilClass。这不是 bug而是 RubyGems 的 API 在大版本迭代中发生了不兼容变更。我的做法是将 RubyGems 版本锁定在 3.0.3.1这是最后一个完美兼容 Ruby 2.5.x 的稳定版