Gemfile.lockをもとに特定のバージョンのRubyで利用できないgemの一覧を出す

Railsアプリなど、Bundlerでgemの依存管理をしているプロジェクトでRubyのアップデートをしようとするとき、不幸なことにgemが全体的に古いことがある。

また、gemspecでrequired_ruby_versionというメタデータでRubyのバージョンの範囲を設定していると、その範囲のRubyの環境下でしかそのgemはインストールできない。よくある例として、次のようにRuby 2系でしか使えないようにしているものがある。

Gem::Specification.new do |spec|
  spec.required_ruby_version = "~> 2.0"
end

このような状況下でRubyだけアップデートするとgemをインストールできないことがある。

こうなると、まずgemをアップデートしていく必要がある。この作業の範囲がどれぐらいか把握するために、Gemfile.lockに載っているgemのうち、特定のバージョンのRubyだとrequired_ruby_versionを満たせないgemの一覧を出せるようなスクリプトを書いた。

# unusable_gems.rb

require 'bundler'

def resolve_gemspec_path(spec)
  # nokogiriはarchとosをgemspecのファイル名に持つ場合がある
  if spec.name == 'nokogiri'
    native_gem_path = Bundler.specs_path.join("#{spec.name}-#{spec.version}-#{Gem::Platform.local.cpu}-#{Gem::Platform.local.os}.gemspec")
    return native_gem_path if native_gem_path.exist?
  end

  case spec.source
  when Bundler::Source::Rubygems
    # default gem
    default_gem_path = Pathname.new(Gem.default_specifications_dir).join("#{spec.name}-#{spec.version}.gemspec")
    return default_gem_path if default_gem_path.exist?

    # ふつうのgem
    Bundler.specs_path.join("#{spec.name}-#{spec.version}.gemspec")
  when Bundler::Source::Git
    # git sourceからインストールしたgem
    Bundler.bundle_path.join("#{spec.source.install_path}", "#{spec.name}.gemspec")
  else
    warn "failed to resolve: #{spec.inspect}"
    nil
  end
end

gemspec_paths = Bundler::LockfileParser.new(Bundler.default_lockfile.read).specs.map { |spec|
  path = resolve_gemspec_path(spec)

  unless path&.exist?
    warn "gemspec not found: #{path}"
    next nil
  end

  path
}.compact

gemspecs = gemspec_paths.map { |path| Gem::Specification.load(path.to_s) }

checked_ruby_version = Gem::Version.create(ARGV[0])
puts gemspecs.reject { |gemspec| gemspec.required_ruby_version.satisfied_by?(checked_ruby_version) }.map { |gemspec| "#{gemspec.name} (#{gemspec.version})" }

例として、まず次のGemfileでbundle installしておく。

source "https://rubygems.org"

gem "activerecord", "7.0.4.2" # required_ruby_version = ">= 2.7.0"

Gemfile.lockが存在するディレクトリで次のように実行する。かなり古いバージョン2.6.0、2.7.0を例として使う。

$ ruby unusable_gems.rb 2.6.0
activemodel (7.0.4.2)
activerecord (7.0.4.2)
activesupport (7.0.4.2)
nokogiri (1.14.1)
oauth (1.1.0)
oauth-tty (1.0.5)

$ ruby unusable_gems.rb 2.7.0
# 2.7.0だと使えないgemがないので結果は空

たとえば間接依存のoauth 1.1.0も https://github.com/oauth-xx/oauth-ruby/blob/v1.1.0/oauth.gemspec#L36 を見るとわかるように2.7.0以上の制限がある。

注意点としては、今回必要だったユースケースではgemのソースの種類としてパス (Bundler::Source::Path) などはなかったので入れていない。また、nokogiriのような特殊なパターンもまだあるかもしれない。