Bundler 工程手册:把 Ruby 依赖变成可重建合同
Ruby 项目最危险的依赖问题,往往不是 bundle install 直接失败,而是它在开发机上成功得过于轻松。全局 gem 恰好存在,命令没有经过 bundle exec,本机已经装好编译工具链,私有源凭证又藏在用户配置里。换到一台干净 CI 主机后,这些隐含输入同时消失,团队才发现仓库并没有保存一份完整的构建合同。
Bundler 的价值不在于“帮 Ruby 下载几个 gem”,而在于把依赖意图、解析结果、平台变体、安装位置和命令运行环境连成可验证链路。真正可靠的判断不是开发者电脑里能不能启动,而是干净环境能否根据仓库事实恢复同一依赖图,并在明确的平台和凭证边界内运行测试。
先分清 Ruby、RubyGems 和 Bundler 各自负责什么
Ruby 是解释器和运行时;RubyGems 定义 gem 包格式、安装目录、索引与 gem 命令;Bundler 读取项目的 Gemfile,求解满足约束的依赖集合,把结果写入 Gemfile.lock,再建立只允许这组依赖进入进程的运行环境。
这三个入口看起来相邻,却不能互换:
ruby 决定语言版本、引擎、ABI 和扩展加载能力。gem install 面向当前 Ruby 的 gem 仓库安装单个包,不理解项目完整 lockfile。bundle install 面向项目恢复依赖图。
bundle exec 在已解析的 bundle 环境里启动命令,避免误用全局可执行文件和 gem。
现代 Ruby 通常随发行版提供 Bundler,但“系统里有 bundle”不等于版本适合当前项目。进入仓库后先核对四个事实:
ruby --version
gem --version
bundle --version
ruby -e 'puts Gem.dir; puts Gem.path'再查看 lockfile 末尾的 BUNDLED WITH,并结合项目的 Ruby 版本文件确认入口:
tail -n 8 Gemfile.lock
bundle platform --ruby
bundle envbundle env 会包含路径、版本和配置摘要,适合排障取证,但分享前必须删除内部源地址、用户名、项目路径和可能暴露组织结构的 gem 名称。
安装入口必须服从项目版本,而不是全局便利
Ruby 版本应由团队选定的版本管理器、受控开发镜像或系统包提供。Bundler 的入门文档说明现代 Ruby 通常预装 Bundler;当项目要求的 Bundler 不存在时,可以通过 RubyGems 安装明确版本:
gem install bundler --version "$(awk '/BUNDLED WITH/{getline; gsub(/^[ \t]+/, ""); print}' Gemfile.lock)"
bundle --version这条命令适合 POSIX 环境,Windows PowerShell 可以先人工读取 BUNDLED WITH,再执行:
gem install bundler --version '<lockfile 中的版本>'
bundle --version如果仓库尚无 lockfile,不要把“本机最新”直接当作团队基线。先从 Bundler 的兼容说明确认 Ruby、RubyGems 与 Bundler 的支持关系,再由一次受审查的依赖初始化提交同时引入 Gemfile.lock 和工具版本约束。
容器开发也不能只锁镜像标签。日志至少打印 Ruby 与 Bundler 版本,镜像升级时重新生成并审查 lockfile,而不是让启动脚本静默安装任意最新 Bundler。
卸载旧版本前先列出可用版本:
gem list '^bundler$' --all
gem uninstall bundler --version '<待移除版本>'Ruby 自带的 default gem 可能不能像普通 gem 一样删除。此时保留发行版版本,通过项目 lockfile 选择目标 Bundler,比破坏系统 Ruby 更稳妥。
用一个最小项目证明依赖链路
在可删除目录中初始化项目:
mkdir bundler-lab
cd bundler-lab
bundle init将 Gemfile 改为:
# frozen_string_literal: true
source "https://rubygems.org"
ruby ">= 3.2"
gem "rake", "~> 13.0"创建 Rakefile:
task :verify do
require "rake"
puts "bundler-model-ok #{Rake::VERSION}"
end恢复、检查并运行:
bundle install
bundle check
bundle exec rake verify
bundle list预期结果包括生成 Gemfile.lock、bundle check 报告依赖满足,以及任务输出 bundler-model-ok。随后验证“没有 bundle 环境”和“进入 bundle 环境”不是同一件事:
which rake || true
bundle exec which rake
bundle exec ruby -e 'puts Gem.loaded_specs.fetch("rake").full_name'清理练习目录即可删除项目事实;如果曾配置本地安装路径,还要先检查 .bundle/config 和 vendor/bundle。不要为了清理单个练习项目直接删除整个用户级 gem 目录。
Gemfile 表达意图,lockfile 保存一次完整求解
Gemfile 记录直接依赖、允许版本范围、来源、分组、平台和 Ruby 条件。Gemfile.lock 则保存解析器选中的直接与传递依赖、来源、平台集合、依赖关系以及 Bundler 版本标记。
因此,日常操作要区分“恢复”和“重新求解”:
# 按现有 lockfile 恢复;Gemfile 有新增约束时可能保守更新相关部分
bundle install
# 查看可更新项,不改 lockfile
bundle outdated
# 只更新目标 gem 及必要依赖
bundle update rack
# 只修改 lockfile,不安装
bundle lock --update rack大范围 bundle update 会重新打开整个求解空间。它不是日常安装命令,也不应该和业务修改混在同一个难以回滚的提交里。依赖变更提交至少展示:
Gemfile 的意图变化。Gemfile.lock 中实际变化的直接与传递依赖。安全公告、兼容问题或功能需求对应的升级理由。
测试结果和回滚目标。
应用仓库通常应提交 Gemfile.lock。开发 gem 时,lockfile 是否进入最终发布包与它是否进入源码仓库是两回事;Bundler 的gem 开发说明指出,源码仓库中的 lockfile可以帮助贡献者获得一致开发环境,而 gem 使用者仍以 gemspec 的运行时约束参与自己的依赖求解。
平台不是附注,而是依赖图输入
同一个 gem 名称可能同时提供纯 Ruby 包和多个平台二进制包。Gemfile.lock 的 PLATFORMS 与具体 gem 条目共同决定 Linux、macOS、Windows、不同 CPU 和不同 Ruby 引擎如何恢复依赖。
先查看当前判断:
bundle platform
bundle lock --print | sed -n '/PLATFORMS/,/DEPENDENCIES/p'
gem environment platform团队要在 Linux CI 构建、macOS 开发和 Windows 开发之间共享仓库时,应显式加入目标平台并审查 lockfile:
bundle lock --add-platform x86_64-linux
bundle lock --add-platform arm64-darwin
bundle lock --normalize-platforms
git diff -- Gemfile.lockbundle lock 手册说明 --add-platform 会在没有目标机器的情况下把该平台纳入解析,但它不等于完成了该平台的真实编译和运行测试。平台加入 lockfile 后,CI 仍需覆盖代表性 OS、CPU、Ruby 引擎和原生库组合。
force_ruby_platform 会要求使用 ruby 平台 gem,并可能触发本地编译。它适合验证源码构建能力或规避某个预编译包问题,不适合默认开启来掩盖平台矩阵缺口。
Groups 控制安装集合,不产生多套版本真相
Gemfile 可以按用途分组:
gem "rack"
group :development, :test do
gem "rspec"
gem "rubocop", require: false
end
group :production do
gem "puma"
end开发机安装完整集合,生产镜像排除开发与测试组:
bundle config set --local without 'development test'
bundle install
bundle config get without分组只决定哪些已锁定依赖进入当前安装环境,不会为每个环境生成互相独立的版本图。某个 gem 同时被多个组依赖时,它仍只有一份锁定版本。环境差异如果需要不同版本,通常说明项目边界或兼容策略需要重新设计。
.bundle/config 是机器或工作树配置,不是通用项目 manifest。团队应在 CI 中使用显式环境变量或流水线参数表达分组策略,避免把开发者的本地路径、凭证和平台设置提交进仓库。
bundle exec 解决的是命令身份漂移
直接执行 rake、rspec 或框架 CLI 时,shell 可能找到全局 gem 提供的可执行文件。它的版本与依赖加载路径未必属于当前 Gemfile.lock。bundle exec 会调整环境,使命令在当前 bundle 中解析:
bundle exec rake test
bundle exec rspec
bundle exec ruby scripts/verify.rb高频命令可生成项目级 binstub:
bundle binstubs rake rspec
bin/rake testbinstub 也应随目标 Bundler 和 gem 升级检查。遇到“本机直接执行成功、CI 报版本冲突”时,先比较:
command -v rake
bundle exec which rake
gem which rake
bundle exec ruby -e 'puts Gem.loaded_specs.fetch("rake").full_name'不要用全局重装同版本 gem 来修饰问题,那只会让另一台机器继续失败。
Path 和 Git 依赖会把仓库状态带进依赖图
本地开发可以引用相邻目录:
gem "shared_rules", path: "../shared_rules"也可以引用 Git 仓库:
gem "shared_rules",
git: "https://git.example.com/platform/shared_rules.git",
ref: "0123456789abcdef0123456789abcdef01234567"Path 依赖不会自动把相邻目录内容复制进当前仓库;CI 若没有同样目录结构就无法恢复。Git 依赖应锁定不可变提交,不能只依赖可移动 branch 或 tag。私有 Git 认证应交给 SSH agent、Git credential helper 或 CI 临时身份,不把凭证放进 URL。
准备离线材料时还要验证 path 与 Git 依赖是否真的进入缓存。某些缓存选项和默认行为会随 Bundler 主版本变化,应通过bundle config 手册核对当前版本的 cache_all、cache_all_platforms 和 allow_offline_install 行为。
Source 排序决定依赖混淆风险
Gemfile 应保留一个明确主源,额外源用 source block 或单包 source 绑定:
source "https://rubygems.org"
source "https://gems.example.com" do
gem "company_build_rules"
endBundler 的Gemfile 指南明确提醒不要使用多个全局 source,因为同名 gem 可能从非预期来源安装。内部包应使用受控命名空间、私有源绑定和服务端保留策略共同防止依赖混淆。
检查实际来源:
bundle lock --print
bundle info company_build_rules
grep -nE 'remote:|company_build_rules' Gemfile.lock私有源不可用时,应明确失败。静默回退公共源看似提高可用性,实际会把“内部服务故障”转换成“下载了另一个同名包”的供应链事故。
凭证要独立于 Gemfile、lockfile 和日志
有认证的源可以通过本地 Bundler 配置或对应环境变量注入。配置键中的主机名会映射为大写环境变量,点号转换为双下划线。具体映射和优先级应以当前版本的bundle config 手册为准。
交互式机器可以用受限账号写入用户配置;CI 更适合由密钥系统注入短期环境变量。无论哪种方式,都先检查配置来源:
bundle config list
bundle config get gems.example.com排障完成后撤销临时配置:
bundle config unset --local gems.example.com
bundle config unset --global gems.example.com禁止把认证信息内嵌到 Gemfile source、Git URL、lockfile、镜像层或缓存键。bundle config list、bundle env、HTTP trace 和 CI debug 日志也可能暴露认证信息,日志共享前必须脱敏。
企业代理与私有 CA 属于传输信任问题,不应通过关闭 TLS 校验解决。先确认 Ruby 使用的 CA 文件、代理环境变量和代理是否进行 TLS 检查,再把批准 CA 安装到受控信任链。遇到证书错误时保留主机名、证书链摘要和时间信息,不上传私钥或完整内部证书包。
原生扩展失败要沿编译链排查
Nokogiri、数据库驱动、加密库等 gem 可能包含原生扩展。失败通常不在 Bundler 求解器,而在目标平台缺少编译器、头文件、系统库、pkg-config、SDK 或兼容 ABI。
先保留完整日志并定位失败 gem:
bundle install --verbose
bundle info '<gem-name>'
gem env构建日志中的 mkmf.log 往往比最后一行 “failed to build native extension” 更有价值。检查顺序是:
Ruby 引擎、版本、架构和目标 gem 平台是否匹配。编译器和链接器是否存在,是否与 Ruby 的构建工具链兼容。系统开发包和头文件是否齐全。
代理或镜像是否漏掉平台包,迫使本机源码编译。缓存是否保存了另一平台或另一 Ruby ABI 的产物。
不要把开发机编译出的扩展目录直接复制到不同 OS 或 CPU 的运行环境。容器构建应在与目标运行镜像 ABI 兼容的 builder 阶段编译,再只复制明确的运行依赖和应用产物。
缓存和离线包只能加速,不能替代依赖真值
Bundler 可以把 gem 包缓存到项目目录:
bundle cache
bundle cache --all-platforms随后验证本地安装:
bundle install --local
bundle check--local 不访问远端,只使用本地 RubyGems 缓存和 vendor/cache。成功意味着当前材料足够,不意味着这些材料来自可信源,也不意味着覆盖了所有平台。
离线交付至少保存:
Ruby 与 Bundler 的批准版本和获取方式。Gemfile、Gemfile.lock 与目标平台集合。经过来源和摘要审查的 gem 包。
原生扩展所需系统包、编译工具链和许可证信息。从空缓存恢复的命令、输出和清理步骤。
共享缓存应按 Ruby 引擎、Ruby 版本、平台、CPU、Bundler 主版本和 lockfile 摘要分层。缓存允许读共享、写隔离;来自不受信分支的任务不应覆盖生产构建缓存。
Checksum 把版本锁定推进到内容校验
只锁定 gem 名称和版本,仍然依赖仓库持续返回同一内容。先检查当前 Bundler 是否支持 lockfile checksum:
bundle lock --help | grep -F -- '--add-checksums'支持时,由受控更新任务补充并审查 checksum:
bundle lock --add-checksums
git diff -- Gemfile.lock
bundle installChecksum 变化必须和明确的依赖更新、来源迁移或事故处置对应。不要为了让安装继续而关闭 checksum validation;应先隔离缓存,比较批准源元数据与下载内容,并判断是上游重发、镜像污染还是本地缓存损坏。
Checksum 证明获取内容符合 lockfile 记录,不证明 gem 没有漏洞、恶意生命周期行为或许可证风险。团队仍需用批准的依赖扫描与许可证策略分析 lockfile,并把例外绑定到具体版本、owner 和到期时间。
CI 只恢复锁定结果,不现场创造新结果
CI 的权威入口可以组织成:
set -eu
ruby --version
bundle --version
bundle config set --local path vendor/bundle
bundle config set --local frozen true
bundle config set --local without 'development'
bundle install --jobs 4 --retry 3
bundle check
bundle exec rake testfrozen 禁止安装过程改写 lockfile;deployment 是一组面向部署的组合设置,当前含义应从bundle config 手册核对。团队最好显式记录实际配置:
bundle config list
git diff --exit-code -- Gemfile Gemfile.lock缓存键不能只使用分支名,至少包含 lockfile 摘要、Ruby 版本和平台。CI 成功后应保留测试结果、依赖审计结果、关键版本和最终 lockfile 摘要,而不是上传含凭证的整个用户配置目录。
升级先缩小求解面,再准备成组回滚
单个依赖升级优先使用定向更新:
bundle outdated rack
bundle lock --update rack --conservative
bundle install
bundle exec rake testBundler 自身升级前先提交干净的 Gemfile.lock,再修改 BUNDLED WITH:
bundle update --bundler
git diff -- Gemfile.lock
bundle install
bundle exec rake test主版本升级可能改变默认配置、缓存范围、清理行为或默认子命令。升级评审应比较 bundle config list、干净安装、平台 lock、原生扩展、离线恢复和部署镜像,而不是只比较 bundle --version。
回滚必须同时恢复 Gemfile、Gemfile.lock、Bundler 版本和相关 CI 配置。只把 lockfile 回退而保留新 Bundler,或只降 Bundler 而保留新 lockfile,都可能产生新的解析或配置差异。
从现象回到依赖证据
You must use Bundler ... with this lockfile
当前 Bundler 与 lockfile 要求不匹配。读取 BUNDLED WITH,安装项目要求版本,再确认 shell 实际解析到哪个 bundle。不要直接删除 lockfile重算。
Gemfile.lock 与 Gemfile 不一致
开发模式下执行 bundle install 并审查差异;CI 或部署环境应在 frozen 模式下失败。先确认是遗漏提交,还是非预期脚本修改了 Gemfile。
找不到私有 gem 或出现 401/403
依次检查 source 绑定、DNS、代理、CA、凭证作用域、过期时间和服务端读取权限。不要先把私有 source 改成公共源,也不要把认证串贴到日志。
开发机成功,CI 报平台 gem 不兼容
比较 bundle platform、lockfile 的 PLATFORMS、Ruby 引擎和 CPU 架构。补充目标平台后,在真实矩阵运行安装和测试;清理跨平台污染缓存。
命令加载了错误版本
比较直接命令与 bundle exec 的路径和 Gem.loaded_specs。修正项目入口或 binstub,不靠全局重装碰运气。
缓存存在却无法离线安装
检查缺失的平台包、Git/path 依赖、未缓存传递依赖和 Bundler 配置。用空用户缓存加 --local 做一次独立验证,才能证明 vendor/cache 自给自足。
全局状态会让错误合同长期潜伏
长期开发机拥有大量全局 gem 和历史配置,bundle install 的成功证据很弱。判断标准应是临时 HOME、空缓存或干净容器能否恢复;失败时再逐层加入代理、CA、私有源和缓存,定位真正缺失输入。
平台集合扩大意味着测试责任扩大
向 lockfile 添加平台很容易,但每增加一种平台,就增加一个原生包、ABI 和运行时验证责任。没有代表性运行测试的平台只能标记为解析支持,不能宣称已交付支持。
Source 与凭证错误会扩散到日志和缓存
带认证 URL 可能进入 lockfile、进程列表、代理日志和镜像层。团队要把仓库地址、认证身份和缓存对象分离,并定期用仓库敏感扫描和 CI 日志抽查验证。
Git gem 绕开了正常制品治理
直接依赖 Git 提交可以快速修复,但也可能绕开 gem 签名、制品保留、许可证扫描和下架流程。它应有明确 owner、不可变 ref、迁回正式 gem 的截止条件和失败回滚路径。
原生扩展把应用依赖升级成工具链升级
一个 gem 的小版本更新可能改变预编译平台覆盖,导致 CI 从下载二进制切换为源码编译。升级评审要比较构建时间、系统库、镜像体积和许可证,而不是只看 Ruby API 变更。
缓存写权限决定污染半径
共享可写缓存会让失败重试和不受信分支影响后续构建。生产交付读取经过提升的只读缓存,普通分支写入隔离命名空间;缓存异常时必须能绕过缓存完成干净恢复。
依赖升级没有 owner 就会变成长期冻结
自动更新工具只能提出候选变更,不能替代兼容判断。团队需要为 Ruby 基线、Bundler、私有源、核心 gem 和原生扩展分别指定 owner、升级窗口、阻塞条件与回滚责任。
团队落地检查
Ruby、RubyGems、Bundler 与目标平台均有可查询、可锁定的版本来源。Gemfile 和 Gemfile.lock 成组评审,安装与更新命令职责分离。应用命令统一经过 bundle exec 或受审查 binstub。
目标 OS、CPU、Ruby 引擎和原生扩展进入测试矩阵。私有 source 明确绑定包范围,不允许同名包静默回退公共源。凭证不进入 URL、Gemfile、lockfile、镜像层、缓存键和共享日志。
CI 使用 frozen / deployment 语义恢复 lockfile,并检查仓库无漂移。空缓存、离线材料和缓存旁路至少完成一次恢复演练。升级提交包含依赖差异、平台差异、测试证据和成组回滚点。
退出项目时清理本地配置、短期凭证、工作树缓存和失效 source。
遇到版本兼容、配置优先级、平台解析或升级行为变化时,分别查看 Bundler 的兼容说明、配置手册、lockfile 手册和部署指南,并以项目锁定版本对应的文档为准。
