bundle exec ruby に限定した詳細を詰めます。まず Box の API と、失敗時の gemspec 評価コンテキストを計測します。
Gem の状態が明確に違います(メソッド数 106 vs 238、object_id も別)。namespace を直接特定します。
namespace が確定しました。最後に root 側の状態を確認して、話を閉じます。
計測が揃いました。bundle exec ruby(rake なし)に限定した詳細です。
RUBY_BOX=1 のとき Ruby は少なくとも2つの namespace を持ちます — root(#<Ruby::Box:2,root>)と main(#<Ruby::Box:3,user,main>)。あなたのコードは main で動きます。
bundle exec は子プロセスに BUNDLER_SETUP を渡します。これを拾うのが rubygems.rb の**最終行(1474行目)**です:
require ENV["BUNDLER_SETUP"] if ENV["BUNDLER_SETUP"] && !defined?(Bundler)つまり Bundler の起動が、ユーザーコードが動き出す前・RubyGems 自身のブートストラップの途中に前倒しされます。この時点での両 namespace の状態を gemspec の中から実測すると:
[where] gemspec evaluated in : #<Ruby::Box:3,user,main>
[main] Gem.methods.size=106 Gem::Specification?=nil Bundler?=nil
[root] Gem.methods.size=238 Gem::Specification?="constant" Bundler?="constant"
ここにねじれがあります:
- Bundler は root にロードされている(main からは
Bundlerすら見えない) - 一方 gemspec は main で評価される。Bundler は
Bundler.eval_gemspecでeval(contents, TOPLEVEL_BINDING.dup, path)を使い、TOPLEVEL_BINDINGの self は main のmainオブジェクトだからです - そしてその瞬間の main の RubyGems はまだ組み立て途中です。
Gemは存在するがメソッドは 106 個しかなく(root は 238)、Gem::Specificationは未定義
結果、gemspec の Gem::Specification.new が uninitialized constant Gem::Specification で落ちます。これが最初に遭遇されたエラーです。
その行は main 側の RubyGems を最後まで読み込ませるので、Gem::Specification は確かに見えるようになります。しかし別物です:
current(main) Gem::Specification.object_id = 664
root's Gem::Specification.object_id = 672
same object? false
gemspec が作る spec は main の Gem::Specification のインスタンスです。ところが要求元の Bundler は root にいて、root の Gem::Specification を前提に照合します。クラスが別オブジェクトなので Definition#materialize が「path source から test-unit-3.7.9 が得られなかった」と判断し:
Could not find test-unit-3.7.9 in gemspec at `.`
になります。require "rubygems" は症状を A から B に付け替えただけ、というのはこの意味です。
なお計測中に、この経路では gemspec が 2回評価されるのも観測できました。1回目の require "rubygems" が main 側で rubygems.rb を読み切り、その最終行 1474 が(main では Bundler 未定義なので)もう一度 bundler/setup を呼び、Bundler.setup → path source → gemspec 評価、と再入するためです。
外すと 1474 行目のフックが空振りし、Bundler の起動は bundle exec がもう一つ用意している RUBYOPT=-r<path>/bundler/setup に委ねられます。こちらは gem_prelude の後、ブート完了後に走るため、その時点で main の RubyGems は完成済み(Gem.methods.size=238、Gem::Specification 定義済み)です。同じ namespace の中で完結するのでクラスのねじれも起きません:
[gemspec] defined?(Gem::Specification)="constant" defined?(Bundler)="constant"
[gemspec] Gem.methods.size=238
ok
要するに タイミングの問題です。BUNDLER_SETUP 経路は「RubyGems が main を組み立て終わる前」に Bundler を root で起動してしまい、RUBYOPT 経路は「組み上がった後」に main で起動します。RUBY_BOX なしでは両者に差が出ない(namespace が1つで、どちらの経路でも同じ Gem::Specification になる)ため表面化しません。
RUBY_BOX=1 bundle exec env -u BUNDLER_SETUP ruby -e '...'env -u が効くのは、まさに 1474 行目の条件 ENV["BUNDLER_SETUP"] && を偽にするからです。RUBYOPT は残るので Bundler のセットアップ自体は行われ、bundle exec の gem 解決もそのまま効きます。
前回述べたとおり、これは子プロセスを生成しない ruby 直実行だから成立します。rake を挟むと、rake プロセスが bundler/setup を読んだ時点で Bundler が BUNDLER_SETUP を ENV に再設定するので、孫プロセスがまた同じ罠を踏みます。
test-unit 側に非はなく、test-unit.gemspec の require "rubygems" は削除して構いません(この経路を避ければ不要であることは確認済みです)。根本的には Ruby::Box と RubyGems ブートストラップの相互作用の問題なので、ruby-core に報告するのが筋だと思います。報告する場合、rubygems.rb:1474 が TOPLEVEL_BINDING(main)から見て未完成な RubyGems の上で Bundler を root に起動してしまう、という形で書けば再現手順込みで伝わるはずです。
リポジトリには変更を加えていません。計測は作業ツリーのコピー(scratchpad/fullcopy)で行いました。