`config/bootsnap.rb` の自動読み込みをオプトイン方式に変更
Bootsnapが作業ディレクトリに依存せずに設定ファイルを扱えるよう、config/bootsnap.rb の読み込みを setup 呼び出し側が明示的に指定する方式に変更しました。これにより、HomebrewなどBootsnapを内部利用するツールが、ユーザーの作業ディレクトリにある設定ファイルを誤って読み込む問題が解消されます。
背景
Homebrewが bootsnap 1.24.x にアップグレードしたことで、brew コマンドをRailsプロジェクトのディレクトリから実行すると、そのプロジェクトの config/bootsnap.rb をHomebrewのBootsnapが読み込んでしまう問題が発生しました。HomebrewにはBundlerが存在しないため、config/bootsnap.rb 内で Bundler を参照するコードが実行されると NameError: uninitialized constant Bootsnap::Bundler で失敗します。
問題の根本原因は、Bootsnap.load_config が引数なしで呼ばれ、カレントディレクトリを基準に config/bootsnap.rb を探索していたことです。Homebrewの起動コード(startup/bootsnap.rb)は Bootsnap.setup を呼ぶだけで、設定ファイルを渡す手段がなかったため、ユーザーのプロジェクトディレクトリに偶然 config/bootsnap.rb があれば必ず読み込まれていました。
この問題は、Bootsnapを内部的に利用する任意のツールが同様の影響を受けうるという、設計上の脆弱性を示しています。
技術的な変更
load_config を独立したメソッドから config_path 引数を受け取る形に変更し、設定ファイルの読み込みを呼び出し元が制御できるようにしました。
変更前 — load_config はカレントディレクトリを基準に設定ファイルを探索し、setup の末尾で無条件に呼ばれていました。
def load_config
config_path = File.expand_path(ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb")
if File.exist?(config_path)
require(config_path)
end
end
def setup(
cache_dir:,
# ...
compile_cache_json: (compile_cache_json_unset = true)
)
# ...
load_config
end
変更後 — setup に config_path: キーワード引数(デフォルト nil)が追加され、load_config は config_path が渡された場合のみ読み込みを実行します。
def setup(
cache_dir:,
# ...
compile_cache_json: (compile_cache_json_unset = true),
config_path: nil
)
# ...
load_config(config_path)
end
def load_config(config_path)
if config_path
config_path = File.expand_path(config_path)
if File.exist?(config_path)
require(config_path)
end
end
end
default_setup(Railsアプリが通常使用するエントリポイント)では、従来の挙動を維持するために config_path: ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb" を明示的に渡すよう変更されました。
def default_setup
# ...
setup(
# ...
config_path: ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb",
)
end
CLI(precompile コマンド)も同様に、Bootsnap.load_config の直接呼び出しから Bootsnap.load_config(ENV["BOOTSNAP_CONFIG"] || "config/bootsnap.rb") に変更されています。
設計判断
setup のデフォルト値を nil にする という設計が採用されました。設定ファイルの読み込みはオプトイン(明示的に指定した場合のみ有効)となり、setup を直接呼び出す外部ツールは config_path を渡さない限り設定ファイルを読み込みません。
この判断は後方互換性を精密に管理しています。default_setup 経由で利用するRailsアプリは config_path が自動で渡されるため変更は不要です。一方、HomebrewのようにBootsnapの setup を直接呼び出す側は config_path: nil(デフォルト)のまま動作し、ユーザーの作業ディレクトリに依存しなくなります。また、外部ツールが自身の設定ファイルパスを明示的に渡すことも可能な設計です。
かつては load_config が副作用(カレントディレクトリの参照)を持つプライベートな実装詳細でしたが、今回の変更でその副作用が呼び出し元の責務として外部化されました。
まとめ
この変更は、暗黙的なグローバル状態(カレントディレクトリ)への依存を排除し、設定ファイルの読み込みを呼び出し元の明示的な指定に委ねる設計へのリファクタリングです。Bootsnapを組み込むツール作者にとっては、setup が環境に予期しない副作用をもたらさないという保証を得られる変更といえます。