深入 Ruby 22:gemspec 与可安装的交付边界
系列导航
导读 · 上一篇:21:RubyGems、Bundler 与版本激活 · 下一篇:23:Minitest 与可失败的行为测试 · 完整源码包
源码目录能运行,为什么安装后却失败
ruby -Ilib exe/taskbook stats 在仓库里成功,只说明源码布局符合这次启动方式。打包漏掉一个文件、可执行入口没有声明、运行依赖只写在开发环境,都会让安装后的程序失败。
可靠的包装实验需要离开源码目录。在临时位置安装生成的 gem,然后从另一个临时工作目录分别加载库和启动 CLI。这样才能排除 -Ilib 或当前目录意外提供文件的情况。完整实验是 examples/ruby/labs/22/run.rb,前置为对象接口、代码加载和 Bundler。
Taskbook 使用包名 taskbook-workshop,文件仍叫 taskbook.gemspec,加载入口是 require 'taskbook',命令名是 taskbook。这几个名字服务不同层次,不要求全部相同。实际发布前还要确认公共名称的使用权;本系列只生成和安装本地包,没有远端发布。
gemspec 是包的声明,也是可执行 Ruby
本工程用显式目录范围形成文件清单:
1 | |
这个范围保留库、命令和签名,排除 lab、运行日志、本地依赖缓存以及生成的 gem 文件。教学实验资料仍在仓库中,但消费者调用库不需要带走所有测试产物。文件清单是交付边界的一部分,不能用“整个仓库都装进去”替代选择。
gemspec 本身会被 Ruby 执行。为了获得版本号而加载整个业务库,可能让构建阶段提前要求尚未安装的运行依赖,或触发顶层副作用。这里直接声明简单版本值,避免让包装动作承担业务初始化。大型库可以采用独立、无副作用的版本文件,原则仍是构建信息应该容易读取。
required_ruby_version 是消费者可用解释器的约束。本系列实际验证 3.4.11,所以把教学包约束在 3.4 系列。这个约束不会替读者安装 Ruby,也不会证明每个 3.4 补丁版本都做过测试;具体支持声明仍要与验证矩阵相称。RubyGems specification reference 定义字段含义。
运行依赖和作者工具不要混在一起
Taskbook 的导入与 HTTP 示例调用 json、csv、net-http 和 rack,因此这些包被声明为运行依赖。Minitest、Steep 与调试器用于开发和验证,放在工程 Gemfile 中。这样的区分依据是安装后的公开入口是否需要该组件,不是组件是否在作者电脑里常用。
一个常见包装缺陷是测试期间通过 Gemfile 装入依赖,gemspec 却没有声明。库在源码目录运行正常,安装到新位置便报 LoadError。另一个相反缺陷是把格式化器、测试框架乃至整个实验环境都变成运行依赖,扩大消费者安装量和冲突面。
本例为了固定教学条件,对运行依赖采用精确版本。真实维护的库通常需要根据兼容性测试声明合理范围,不能让一个教程里的冻结选择代替整个生态的依赖策略。Gemfile.lock 记录的是这个工程的验证解,gemspec 记录的是包向消费者表达的约束,两者不能互相替换。
HTTP 示例虽然使用 Rack,最小对象仍只实现 call(env)。是否把它拆成可选扩展包属于后续产品规模的决定;当前项目足够小,一个包更容易复现。这个选择的代价是 CLI 消费者也会看到 HTTP 相关运行依赖,正文明确保留这一点,不把教学包装当成体积最优方案。
检查生成包,不只检查配置源文件
实验用临时路径存放包,不把生成的二进制归档提交为源码:
1 | |
实际脚本通过 Dir.mktmpdir 生成路径,不依赖上述示意目录存在。构建成功之后,读取归档自己的 specification:
1 | |
这些断言观察的是构建结果,而不仅是 spec.files 那行源码。后者看起来正确,仍可能因为运行目录或打包规则导致实际清单不符合预期。阅读输出包是对边界的一次独立检查。
包内入口文件还应允许从任意正常工作目录加载。CLI 使用 require 'taskbook/io',由安装后的加载路径找到库;它不应该假设消费者当前工作目录下有一个 lib 子目录。第 20 篇讨论的路径参照物,在这里变成可交付性问题。
临时安装与真实调用的验证顺序
实验设定新的 GEM_HOME,将本地包安装到临时目录,再启动全新 Ruby 子进程。运行时还保留已经锁定安装好的依赖搜索路径,因此这次验证是“目标包隔离安装,依赖复用已冻结环境”。它不是空机器、断网条件下自动安装所有依赖的证明。
安装时使用 --ignore-dependencies 的原因是依赖已经由 bundle 安装并验证,实验不需要再次访问网络。这个选项不能作为一般安装说明随意照抄;脱离现有依赖环境时,会得到缺依赖的安装。源码到包边界与从零供应链安装是两个验收目标,报告需要分别命名。
子进程显式清除可能把源码 bundle 带入的新进程环境项,并把当前工作目录切换到临时目录。随后读取 Gem.loaded_specs.fetch('taskbook-workshop').full_gem_path,确认路径确实位于临时安装根下。只有 require 没抛异常还不够,它可能误命中了全局已有包。
API 场景调用 JSON 输入解析,断言空任务清单仍是空数组。CLI 场景通过安装生成的命令入口读取标准输入 [],断言统计响应含有零任务结果。一个验证公开函数,一个验证包装后的可执行链路,它们暴露的缺陷不同。RubyGems 打包教程 提供了基础结构,这里的进程实验补上了工程自身的行为约束。
库与命令各自承担什么
库文件被其他 Ruby 程序 require 时,不应直接打印报告或退出进程。否则消费者无法组合调用,也无法自己处理错误。Taskbook 的库返回 Ruby 对象或抛出可分类异常,命令入口才把对象序列化到 stdout,把输入错误写到 stderr,并设置退出码。
可执行文件中的 shebang 是解释器选择入口,安装器生成的命令包装还可能记录解释器路径。测试使用当前 RbConfig.ruby 启动安装后的命令文件,确保验证绑定到所选解释器。仅仅给文件添加执行权限,并不能证明 PATH 查找、shebang 与依赖环境都正确。
版本号增加也不会自动证明兼容。删除关键字参数、改变空结果形式、让导入器开始关闭调用者传入流,都会影响使用者,却未必触发包装错误。发布边界需要行为测试守护,而测试能守护到哪里,取决于实际断言覆盖了哪些公开合同。
发布者承诺与使用者环境
包能够安装,只表明安装器接受它的元数据和文件;消费者调用公开功能时,还会受到操作系统、解释器以及依赖行为的影响。因此支持范围应写成可验证矩阵,而不是在 gemspec 里填一个很宽的 Ruby 下限就声称兼容所有环境。
本教学包把兼容范围收窄到 Ruby 3.4 系列,实际证据来自一个补丁版本与一个平台。它为教学复现提供明确起点,但没有多平台兼容结论。若未来计划支持 Linux 和其他 Ruby 实现,应该各自运行导入、CLI、HTTP 与关闭场景,并记录能力差异,不能仅凭纯 Ruby 文件没有 C 代码就假定行为相同。
版本升级时应考虑公共合同的变化。字段允许范围、异常分类、输出格式、命令选项及资源所有权都属于消费者可能依赖的行为。内部方法名称调整通常不影响消费者,公开方法的关键字变化则可能立即破坏调用。语义化版本号能表达维护者的意图,是否履行意图仍由测试与迁移说明提供证据。
源文件列表之外,许可证和使用说明同样影响交付。教学包声明 MIT,真实复用时仍应确认自己拥有所有纳入文件的授权。不要把依赖的缓存或第三方源码随整个目录打进包,又只保留自己的许可证字段。显式文件范围可以减少无意携带第三方内容,但授权检查仍是独立责任。
本地包装实验没有必要持久保存每次生成的 gem。版本化源码、锁定依赖、构建命令和行为日志更适合审阅;需要分发时再生成可核验产物。若保存产物,应记录摘要和对应源码版本,避免同名文件被覆盖后无法追溯。
安装脚本的诊断同样需要留存。如果生成命令时缺少执行入口,安装器可能仍完成其他文件复制;实际启动测试才能暴露缺陷。把构建、安装、加载和业务调用分成连续判定,可以准确指出失败所在阶段,而不是笼统报告“包不可用”。
运行与练习
1 | |
成功输出以 PASS 22 开头;evidence/22/run.txt 保存构建、隔离安装和实际入口判定。临时目录在完成后清理,本地包不进入远端注册表。复用依赖的边界应与成功结果一起阅读。
练习一:在临时副本的文件清单中删除 lib/taskbook/io.rb,重新打包,确认文件清单断言或安装后调用会失败。练习二:从不同工作目录运行安装后的统计命令,再故意给 CLI 引入 require './lib/taskbook',观察失败并恢复正确入口。两次练习都在临时副本中进行,不修改已经验证的主工程。
