Bootsnapにカスタムコンパイラフックを追加:ファイルごとにRubyコンパイラを切り替え可能に

rails/bootsnap

Bootsnap::CompileCache::ISeq.compiler_selector にProcを設定することで、Rubyファイルの読み込みごとにコンパイラを動的に選択できるフックAPIが追加されました。これにより、アプリケーションコードのみfrozen string literalを有効化するといった、ファイルパスに基づく細粒度なコンパイルオプションの制御が可能になります。

背景

これまでBootsnapのISeqコンパイルキャッシュは、全ファイルに対して単一のコンパイラを使用していました。frozen_string_literal: true を有効化したい場合、プロジェクト全体に適用するしかなく、gemの挙動に影響を与えるリスクがありました。アプリケーションコードだけに適用するような選択的な制御手段が存在しなかったのです。

このPRはその制約を解消するフックAPIを導入しています。また同時に、設定を外部ファイルに切り出す BOOTSNAP_CONFIG 環境変数とコンフィグファイル機構(デフォルト: config/bootsnap.rb)も追加されており、起動スクリプトを汚染せずにBootsnapの挙動をカスタマイズできる基盤が整えられました。

技術的な変更

コンパイラのクラス化と3種のデフォルト実装

これまでモジュール直接メソッドとして実装されていたコンパイルロジックが、Bootsnap::CompileCache::ISeq::Compiler クラスに切り出されました。このクラスは namespacecompile_options を受け取り、インスタンスメソッドとして input_to_storage を持ちます。

デフォルトのコンパイラ定数として以下の3種が提供されます:

  • Bootsnap::CompileCache::ISeq::DEFAULT — 標準コンパイラ
  • Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERALfrozen_string_literal: true を有効化
  • Bootsnap::CompileCache::ISeq::MUTABLE_STRING_LITERALfrozen_string_literal: false を明示

これらはそれぞれ異なる compile_options を持つ Compiler インスタンスです。テストコードでも Bootsnap::CompileCache::ISeq 直接へのモックが Bootsnap::CompileCache::ISeq::DEFAULT へのモックに変更されており、責務の分離が明確になっています。

compiler_selector によるファイルごとのコンパイラ切り替え

Bootsnap::CompileCache::ISeq.compiler_selector= にProcを設定すると、Rubyファイルが読み込まれるたびにそのProcがパスを引数として呼び出され、使用するコンパイラインスタンスを返します。

gems_root = File.join(Bundler.bundle_path.cleanpath, "")
app_root  = File.join(Dir.pwd, "")

Bootsnap::CompileCache::ISeq.compiler_selector = ->(path) do
  if path.start_with?(app_root) && !path.start_with?(gems_root)
    Bootsnap::CompileCache::ISeq::FROZEN_STRING_LITERAL
  else
    Bootsnap::CompileCache::ISeq::DEFAULT
  end
end

compiler_selectornil の場合は従来通り default_compiler が使用されるため、既存の挙動との後方互換性は保たれます。

ネイティブ層への namespace パラメータ追加

Cの拡張ライブラリ側でも変更が加えられています。bs_rb_fetchbs_rb_precompile の関数シグネチャに namespace_v パラメータが追加され、それぞれ引数数が4→5、3→4に増加しました。

変更前:

static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler, VALUE args);
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE path_v, VALUE handler);

変更後:

static VALUE bs_rb_fetch(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler, VALUE args);
static VALUE bs_rb_precompile(VALUE self, VALUE cachedir_v, VALUE namespace_v, VALUE path_v, VALUE handler);

bs_cache_path も同様に namespace_v を受け取るよう変更されており、コンパイラごとに異なるキャッシュ名前空間を割り当てる仕組みの基盤となっています。これにより、同一ファイルに対して異なるコンパイラで生成されたISeqが別々にキャッシュされます。

input_to_output インターフェースの変更

ハンドラの input_to_output メソッドに path 引数が追加されました。YAMLの各実装クラスでは _path として受け取り(現時点では未使用)、テストヘルパーも同様に更新されています。

変更前:

def self.input_to_output(_data, _kwargs)
  raise("but why tho")
end

変更後:

def self.input_to_output(_source, _path, _kwargs)
  raise("but why tho")
end

この変更はカスタムコンパイラがパスを参照できるようにするための統一的なインターフェース整備です。

enable_frozen_string_literal ヘルパーメソッド

最も一般的なユースケースをカバーするため、Bootsnap.enable_frozen_string_literal が追加されました。

Bootsnap.enable_frozen_string_literal(app_only: true)

app_only: true を渡すと compiler_selector に前述のパスベースのラムダが自動設定されます。app_only: false(デフォルト)の場合は default_compilerFROZEN_STRING_LITERAL に設定するだけのシンプルな動作になります。

コンフィグファイル機構

Bootsnap.load_config メソッドが追加され、Bootsnap.setup の末尾および bootsnap precompile CLIコマンドの実行時に自動的に呼び出されます。読み込むファイルのパスは BOOTSNAP_CONFIG 環境変数で上書き可能で、デフォルトは config/bootsnap.rb です。

設計判断

compiler_selector をモジュール属性のProcとして実装する方式が採用されました。コンパイラ自体はクラスインスタンスとして定義しつつ、選択ロジックはProcに委譲することで、カスタムコンパイラの実装にRubyの任意のオブジェクトを利用できます。

キャッシュの名前空間化はこの設計の重要な補完要素です。compiler_selector が異なるコンパイラを返す場合、キャッシュキーにコンパイラの namespace が組み込まれることで、コンパイルオプションの違いを反映したキャッシュが生成されます。同一ファイルのデフォルトコンパイル結果とfrozen string literal有効化のコンパイル結果が混在してキャッシュが汚染されるリスクを回避しています。

PR説明ではコンパイラインターフェースの安定性について「歴史的にほとんど変更されていないが、安定性は保証しない」と明示されています。これはカスタムコンパイラの実装者への適切な警告であり、拡張性と安定性保証のバランスを取った判断といえます。

まとめ

このPRは、Bootsnapのコンパイルキャッシュ基盤に「コンパイラをファイルごとに選択する」層を追加したものです。compiler_selector フックとキャッシュ名前空間の組み合わせにより、アプリケーションコードとgemコードで異なるコンパイルオプションを安全に共存させることが可能になり、frozen string literal導入の障壁を下げるという実用的な恩恵をもたらします。

記事メタデータ

Generated by:
Claude Sonnet 4.6 for DiffDaily
LLM Trace:
ebf23de7

この記事はAIによって自動生成されています。内容の正確性については、必ずソースコードやPRを確認してください。

品質レビュー結果

Review Status:
承認済み
Review Count:
1回
Reviewed by:
Gemini 2.5 Pro for DiffDaily

Review Criteria:

記事構成 ✓ PASS

Title, Context, Technical Detailの存在と明確さ

リード文(総論)、背景・技術的な変更・設計判断(各論)、まとめ(結論)という「総論→各論→結論」の3部構成が明確に適用されており、模範的な記事構成です。

カスタムMarkdown構文 ✓ PASS

シンタックスハイライト・GitHubリンク記法の正確性

ファイル名付きシンタックスハイライト(例: ```ruby:config/bootsnap.rb)、PR番号のリンク記法([PR #535](...))が正しく使用されています。

対象読者への適合性 ✓ PASS

エンジニア向けの適切な技術レベルと表現

Bootsnap、ISeq、コンパイルキャッシュ等の専門用語を前提としており、対象読者である専門知識を持つエンジニアに適合した内容と表現になっています。

パラグラフ・ライティング ✓ PASS

トピックセンテンス・1段落1トピック・段落長

各セクション、各パラグラフが論理的に構成されています。特に各段落の冒頭にトピックセンテンスが置かれているため、非常に読みやすいです。

Diff内容との照合 ✓ PASS

コードブロックとDiff内容の一致

Rubyコード、Cの関数シグネチャ、テストヘルパーのコードスニペットなど、提供されたDiffの内容を正確に引用・反映しています。

技術用語の正確性 ✓ PASS

技術用語の正確な使用

`compiler_selector`、`namespace`、`input_to_output`などの専門用語や固有名詞が、PRの文脈に沿って正確に使用されています。

説明の技術的正確性 ✓ PASS

技術的主張の正確性と論理性

コンパイラのクラス化、C拡張への`namespace`追加、コンフィグファイル機構の導入など、技術的な変更点に関する説明がDiffの内容と完全に一致しており、技術的に正確です。

事実の突合 ✓ PASS

PR情報による主張の裏付け(ハルシネーション検出)

記事内のすべての主張は、PRのDescriptionやDiff内のコードで裏付けられています。特に「設計判断」のセクションは、コードの変更意図を深く読み解いた質の高い解説であり、ハルシネーションは見られません。

数値・固有名詞の確認 ✓ PASS

PR番号・コミットID・バージョン等の正確性

PR番号(#535)や、コード内の定数名、メソッド名などの固有名詞はすべて正確に記載されています。

タイトル・説明との一致 ✓ PASS

記事タイトル・説明とPR内容の一致

PRのタイトル「Allow to substitute the Ruby compiler」を、「Bootsnapにカスタムコンパイラフックを追加:ファイルごとにRubyコンパイラを切り替え可能に」と、より具体的に分かりやすく表現しており、内容と完全に一致しています。

外部知識の正確性 ✓ PASS

PRに記載のない外部知識(LTS、サポート状況など)の不使用

記事の内容はPR情報(Description、Diff)に完全に準拠しており、バージョンサポート状況やリリース日程といったPR外の知識の追記はありません。

時間表現の正確性 ✓ PASS

時間表現がPR情報と一致しているか

「既に」「近い将来」といった時間表現の歪曲はなく、PRの事実関係を正確に記述しています。